import { sql, type Selectable } from 'kysely'
import * as Id from '../../internal/Id.js'
import type * as Db from '../Db.js'
import type * as db_Schema from '../Schema.js'
/** Columns of the `projects` table, derived from `Schema.Project`. */
export type Table = db_Schema.Project
/** A stored project row. */
export type Record = Selectable
/**
* Inserts a project and returns the stored record.
*
* @param db - The database.
* @param input - The project to insert.
* @returns The stored record.
*/
export function create(db: Db.Db, input: create.Input): Promise {
const now = new Date().toISOString()
return db.kysely
.insertInto('projects')
.values({
createdAt: now,
id: Id.generate('prj'),
name: input.name,
orgId: input.orgId,
sponsorshipSubsidyEndsAt: null,
sponsorshipSubsidyStartsAt: null,
sponsorshipSpendLimit: null,
updatedAt: now,
})
.returningAll()
.executeTakeFirstOrThrow()
}
export declare namespace create {
/** Fields accepted when inserting a project. */
type Input = {
/** Human-readable project name. */
name: string
/** Owning organization id (`org_…`). */
orgId: string
}
}
/**
* Deletes a project.
*
* @param db - The database.
* @param id - The project id (`prj_…`).
* @returns Whether the project existed and was deleted.
*/
export async function deleteProject(db: Db.Db, id: string): Promise {
const result = await db.kysely.deleteFrom('projects').where('id', '=', id).executeTakeFirst()
return result.numDeletedRows > 0n
}
/**
* Reads a project by id.
*
* @param db - The database.
* @param id - The project id (`prj_…`).
* @returns The record, or `undefined` when absent.
*/
export function get(db: Db.Db, id: string): Promise {
return db.kysely.selectFrom('projects').selectAll().where('id', '=', id).executeTakeFirst()
}
/**
* Sets a project's sponsorship activation time once. Concurrent first
* requests converge on the same stored timestamp.
*
* @param db - The database.
* @param options - Project ownership and activation time.
* @returns The project with its stable activation time, or `undefined` when absent.
*/
export function activateSponsorship(
db: Db.Db,
options: activateSponsorship.Options,
): Promise {
return db.kysely
.updateTable('projects')
.set({
sponsorshipSpendLimit: sql`case when sponsorship_subsidy_starts_at is null then ${options.spendLimit} else sponsorship_spend_limit end`,
sponsorshipSubsidyEndsAt: sql`coalesce(sponsorship_subsidy_ends_at, ${options.endsAt})`,
sponsorshipSubsidyStartsAt: sql`coalesce(sponsorship_subsidy_starts_at, ${options.at})`,
updatedAt: sql`case when sponsorship_subsidy_starts_at is null then ${options.at} else updated_at end`,
})
.where('id', '=', options.id)
.where('orgId', '=', options.orgId)
.returningAll()
.executeTakeFirst()
}
export declare namespace activateSponsorship {
/** Project activation inputs. */
type Options = {
/** Activation time used only when the project has not started its window. */
at: string
/** End of the promotion window, stored with the first activation. */
endsAt: string
/** Project id (`prj_…`). */
id: string
/** Owning organization id (`org_…`). */
orgId: string
/** Optional promotion cap in fee-token base units, stored only on first activation. */
spendLimit: string | null
}
}
/**
* Reads projects by id for operational attribution displays.
*
* @param db - The database.
* @param ids - Project ids to read.
* @returns Matching project records.
*/
export function listByIds(db: Db.Db, ids: readonly string[]): Promise {
if (ids.length === 0) return Promise.resolve([])
return db.kysely
.selectFrom('projects')
.selectAll()
.where('id', 'in', [...ids])
.execute()
}
/**
* Lists an organization's projects, newest first.
*
* @param db - The database.
* @param orgId - The owning organization id (`org_…`).
* @returns The records.
*/
export function listByOrg(db: Db.Db, orgId: string): Promise {
return db.kysely
.selectFrom('projects')
.selectAll()
.where('orgId', '=', orgId)
.orderBy('createdAt', 'desc')
.execute()
}
/**
* Lists a bounded page of an organization's projects, newest first.
*
* @param db - The database.
* @param orgId - The organization id.
* @param options - Keyset and page-size options.
* @returns At most `limit + 1` rows for next-cursor derivation.
*/
export function listByOrgPage(
db: Db.Db,
orgId: string,
options: listByOrgPage.Options,
): Promise {
const cursor = options.cursor
let query = db.kysely.selectFrom('projects').selectAll().where('orgId', '=', orgId)
if (cursor)
query = query.where((eb) =>
eb.or([
eb('createdAt', '<', cursor.createdAt),
eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
]),
)
return query
.orderBy('createdAt', 'desc')
.orderBy('id', 'desc')
.limit(options.limit + 1)
.execute()
}
export declare namespace listByOrgPage {
/** Cursor fields for the last project on the previous page. */
type Cursor = {
/** Project creation time. */
createdAt: string
/** Project id, used as a deterministic tie-breaker. */
id: string
}
/** Options for {@link listByOrgPage}. */
type Options = {
/** Last project returned by the previous page. */
cursor?: Cursor | undefined
/** Requested page size. */
limit: number
}
}
/**
* Renames a project, bumping `updatedAt`.
*
* @param db - The database.
* @param id - The project id (`prj_…`).
* @param input - The fields to update.
* @returns The updated record, or `undefined` when absent.
*/
export function update(db: Db.Db, id: string, input: update.Input): Promise {
return db.kysely
.updateTable('projects')
.set({ name: input.name, updatedAt: new Date().toISOString() })
.where('id', '=', id)
.returningAll()
.executeTakeFirst()
}
export declare namespace update {
/** Mutable project fields. */
type Input = {
/** New human-readable project name. */
name: string
}
}