diff --git a/package.json b/package.json index 111e70a..7bf98f8 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "tool:cleanupSessions": "tsx tools/cleanupSessions.ts", "tool:pruneWebhookLogs": "tsx tools/pruneWebhookLogs.ts", "tool:seed-mirror": "tsx tools/seedMirror.ts", + "tool:verifyRecordSharing": "tsx tools/verifyRecordSharing.ts", "cli": "tsx src/cli/cli.ts", "test": "vitest run", "test:watch": "vitest", diff --git a/src/api/agent.ts b/src/api/agent.ts index accb21c..de8631f 100644 --- a/src/api/agent.ts +++ b/src/api/agent.ts @@ -7,6 +7,7 @@ import { getLatestReadyVersion, hasOrgAccess, loadVersionSchemas, + recordsVersionId, } from '../lib/version-helpers.server.js' const DEFAULT_SCHEMA_SLUG = 'update' @@ -119,7 +120,7 @@ export async function agentPage(c: Context) { schema.recordObjects, eq(schema.versionRecords.recordHash, schema.recordObjects.hash), ) - .where(eq(schema.versionRecords.versionId, latest.id)) + .where(eq(schema.versionRecords.versionId, recordsVersionId(latest))) .limit(3) examples = rows.map((r) => ({ id: r.recordId, type: r.type, data: r.data })) } diff --git a/src/api/ark.ts b/src/api/ark.ts index 82536d9..7706f4f 100644 --- a/src/api/ark.ts +++ b/src/api/ark.ts @@ -15,6 +15,7 @@ import { filterTypeSchema, getPrivateFields, parseSemver, + recordsVersionId, } from '../lib/version-helpers.server.js' import { type AuthEnv } from './auth.server.js' @@ -96,6 +97,7 @@ export async function resolve(c: Context) { appId: string | null actorId: string | null createdAt: Date + recordsFromVersionId: number | null } | null = null if (version !== undefined) { @@ -110,6 +112,7 @@ export async function resolve(c: Context) { appId: schema.versions.appId, actorId: schema.versions.actorId, createdAt: schema.versions.createdAt, + recordsFromVersionId: schema.versions.recordsFromVersionId, }) .from(schema.versions) .where( @@ -133,6 +136,7 @@ export async function resolve(c: Context) { appId: schema.versions.appId, actorId: schema.versions.actorId, createdAt: schema.versions.createdAt, + recordsFromVersionId: schema.versions.recordsFromVersionId, }) .from(schema.versions) .where( @@ -183,7 +187,7 @@ export async function resolve(c: Context) { // were denormalized this scanned every version_records row for the // version just to resolve one ARK. and( - eq(schema.versionRecords.versionId, versionRow.id), + eq(schema.versionRecords.versionId, recordsVersionId(versionRow)), eq(schema.versionRecords.recordId, recordId), eq(schema.versionRecords.type, recordType), ), diff --git a/src/api/collections.ts b/src/api/collections.ts index 1d36528..d58a535 100644 --- a/src/api/collections.ts +++ b/src/api/collections.ts @@ -19,6 +19,7 @@ import { getPrivateTypes, hasOrgAccess, loadVersionSchemas, + recordsVersionId, } from '../lib/version-helpers.server.js' import { type AuthEnv } from './auth.server.js' import { fullPrincipalUserId, requireAuth, requireUnscopedKey } from './auth.server.js' @@ -410,7 +411,7 @@ const app = new Hono() count: sql`count(*)::int`, }) .from(schema.versionRecords) - .where(eq(schema.versionRecords.versionId, latestVersion.id)) + .where(eq(schema.versionRecords.versionId, recordsVersionId(latestVersion))) .groupBy(schema.versionRecords.type) typeCounts = rows.map((r) => ({ type: r.type, count: r.count })) } @@ -946,7 +947,7 @@ const app = new Hono() const types = await db .selectDistinct({ type: schema.versionRecords.type }) .from(schema.versionRecords) - .where(eq(schema.versionRecords.versionId, version.id)) + .where(eq(schema.versionRecords.versionId, recordsVersionId(version))) // tar needs each entry's byte length in its header, so an entry cannot be // written from an unbounded stream. Previously every record of a type was @@ -1006,7 +1007,7 @@ const app = new Hono() // Walk the (version_id, type, record_id) index; record_objects is // joined only to pick up the body for the rows on this page. const conditions = [ - eq(schema.versionRecords.versionId, version.id), + eq(schema.versionRecords.versionId, recordsVersionId(version)), eq(schema.versionRecords.type, type), ] // Non-owners never see records flagged private (per-version flag). @@ -1212,7 +1213,7 @@ const app = new Hono() .from(schema.versionRecords) .where( and( - eq(schema.versionRecords.versionId, latestVersion.id), + eq(schema.versionRecords.versionId, recordsVersionId(latestVersion)), eq(schema.versionRecords.private, true), ), ) @@ -1221,7 +1222,7 @@ const app = new Hono() .from(schema.versionRecords) .where( and( - eq(schema.versionRecords.versionId, latestVersion.id), + eq(schema.versionRecords.versionId, recordsVersionId(latestVersion)), sql`${schema.versionRecords.publicRecordHash} IS NOT NULL`, ), ) @@ -1277,11 +1278,15 @@ const app = new Hono() // Copy the record set server-side. A fork of a multi-million-record // collection has no reason to round-trip every row through the app. + // + // A real copy, not a shared pointer: the fork is an independent + // collection, and sharing across collections would make the source's + // rows undeletable for as long as any fork existed. await tx.execute(sql` INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type, private) SELECT ${newVersion!.id}, record_hash, public_record_hash, record_id, type, private FROM version_records - WHERE version_id = ${latestVersion.id} + WHERE version_id = ${recordsVersionId(latestVersion)} `) const sourceFiles = await tx diff --git a/src/api/files.ts b/src/api/files.ts index c01ea31..c871e87 100644 --- a/src/api/files.ts +++ b/src/api/files.ts @@ -91,6 +91,11 @@ async function isFilePubliclyAccessible( data: schema.recordObjects.data, }) .from(schema.versionRecords) + // Ownership-only join: this asks whether the file is referenced anywhere in + // THIS collection, and a version sharing another's rows is in the same + // collection as the version that owns them. The `versionId` it selects is + // used to load that version's schemas for privacy, and a metadata patch has + // the same schema set as its base, so the filtering is unchanged too. .innerJoin(schema.versions, eq(schema.versionRecords.versionId, schema.versions.id)) .innerJoin( schema.recordObjects, diff --git a/src/api/negotiate.ts b/src/api/negotiate.ts index 4588eef..91ce67d 100644 --- a/src/api/negotiate.ts +++ b/src/api/negotiate.ts @@ -10,6 +10,7 @@ import { canonicalize, checkSchemaBounds, deriveSemver, + describeError, filterRecordData, filterTypeSchema, findExtraFields, @@ -21,6 +22,7 @@ import { hasOrgAccess, loadVersionSchemas, parseSemver, + recordsVersionId, resolveCollection, type SchemaEntry, stripToSchema, @@ -132,37 +134,6 @@ async function ingestManifestEntries( return deduped.filter((r) => !existingRecordSet.has(r.hash)).map((r) => r.hash) } -/** - * Flatten an error and its `cause` chain into one string. - * - * Drizzle puts the SQL in `message` and the underlying Postgres error — the part - * that says *why* — in `cause`. Recording only `message` yields "Failed query: - * SELECT …" with no reason, which is unactionable for whoever is reading a - * failed session hours later. - */ -function describeError(err: unknown): string { - const parts: string[] = [] - let current: unknown = err - for (let depth = 0; current && depth < 5; depth++) { - if (current instanceof Error) { - parts.push(current.message) - // Postgres errors carry the useful specifics outside `message`. - const pg = current as { code?: string; detail?: string; hint?: string } - const extra = [ - pg.code && `code ${pg.code}`, - pg.detail && `detail: ${pg.detail}`, - pg.hint && `hint: ${pg.hint}`, - ].filter(Boolean) - if (extra.length > 0) parts.push(extra.join(', ')) - current = current.cause - } else { - parts.push(String(current)) - break - } - } - return parts.join(' | ') -} - /** Mirrors `c.json(body, status)` so the finalize body reads unchanged. */ const reply = (body: unknown, status: ContentfulStatusCode = 200) => ({ status, body }) @@ -1041,7 +1012,7 @@ app.post( ? sql`NOT m.submitted` : sql`NOT m.submitted AND NOT EXISTS ( SELECT 1 FROM version_records vr - WHERE vr.version_id = ${latest.id} AND vr.record_hash = m.hash + WHERE vr.version_id = ${recordsVersionId(latest)} AND vr.record_hash = m.hash )` const WALK_BATCH = 5000 @@ -1294,13 +1265,13 @@ app.post( SELECT (SELECT count(DISTINCT coalesce(final_hash, hash)) FROM negotiate_session_manifest WHERE session_id = ${sessionId}) AS new_count, - (SELECT count(*) FROM version_records WHERE version_id = ${latest.id}) AS old_count, + (SELECT count(*) FROM version_records WHERE version_id = ${recordsVersionId(latest)}) AS old_count, EXISTS ( SELECT 1 FROM negotiate_session_manifest m WHERE m.session_id = ${sessionId} AND NOT EXISTS ( SELECT 1 FROM version_records vr - WHERE vr.version_id = ${latest.id} + WHERE vr.version_id = ${recordsVersionId(latest)} AND vr.record_hash = coalesce(m.final_hash, m.hash) ) ) AS has_new diff --git a/src/api/query.ts b/src/api/query.ts index 753046f..9e13c47 100644 --- a/src/api/query.ts +++ b/src/api/query.ts @@ -10,6 +10,7 @@ import { getPrivateTypes, hasOrgAccess, parseSemver, + recordsVersionId, type SchemaEntry, } from '../lib/version-helpers.server.js' import { type AuthEnv, fullPrincipalUserId } from './auth.server.js' @@ -108,6 +109,7 @@ async function getOrBuildSqlite( id: schema.versions.id, semver: schema.versions.semver, recordCount: schema.versions.recordCount, + recordsFromVersionId: schema.versions.recordsFromVersionId, }) .from(schema.versions) .where( @@ -163,7 +165,7 @@ async function getOrBuildSqlite( // Load records (excluding private types and private records for non-owners). // Record-level privacy is the per-version version_records.private flag. - const recordConditions = [eq(schema.versionRecords.versionId, version.id)] + const recordConditions = [eq(schema.versionRecords.versionId, recordsVersionId(version))] if (!ownerAccess) { recordConditions.push(eq(schema.versionRecords.private, false)) for (const pt of privateTypes) { diff --git a/src/api/records.ts b/src/api/records.ts index 56b0d6c..bb16f12 100644 --- a/src/api/records.ts +++ b/src/api/records.ts @@ -1,4 +1,4 @@ -import { and, eq, inArray, sql } from 'drizzle-orm' +import { and, eq, inArray, or, sql } from 'drizzle-orm' import { Hono } from 'hono' import { openApi } from 'hono-zod-openapi' import { streamText } from 'hono/streaming' @@ -36,6 +36,9 @@ async function resolvePublicHashes(hashes: string[]): Promise() versionCreatedAt: schema.versions.createdAt, }) .from(schema.versionRecords) - .innerJoin(schema.versions, eq(schema.versionRecords.versionId, schema.versions.id)) + // A version contains this record if it owns the row OR shares the rows of + // the version that does. Matching only on ownership would drop every + // metadata-only patch version from the provenance list, even though the + // record is just as much a part of it. + .innerJoin( + schema.versions, + or( + eq(schema.versionRecords.versionId, schema.versions.id), + eq(schema.versionRecords.versionId, schema.versions.recordsFromVersionId), + ), + ) .innerJoin(schema.collections, eq(schema.versions.collectionId, schema.collections.id)) .innerJoin( schema.organization, diff --git a/src/api/versions.ts b/src/api/versions.ts index 9961947..7f80905 100644 --- a/src/api/versions.ts +++ b/src/api/versions.ts @@ -2,6 +2,7 @@ import { and, eq, sql } from 'drizzle-orm' import { Hono } from 'hono' import { openApi } from 'hono-zod-openapi' import { stream } from 'hono/streaming' +import type { ContentfulStatusCode } from 'hono/utils/http-status' import { z } from 'zod' import { db, schema } from '../db/client.server.js' @@ -9,6 +10,7 @@ import { buildArkUrl, DEFAULT_NAAN } from '../lib/ark.js' import { canonicalize, deriveSemver, + describeError, filterRecordData, filterSchemasForPublic, filterTypeSchema, @@ -19,6 +21,7 @@ import { hasOrgAccess, loadVersionSchemas, parseSemver, + recordsVersionId, resolveAccessibleCollection, resolveCollection, type SchemaEntry, @@ -29,6 +32,22 @@ import { type AuthEnv, requireAuth } from './auth.server.js' const MAX_METADATA_BYTES = 64 * 1024 +/** + * Drop columns that are storage mechanics rather than part of the version a + * caller sees. + * + * `recordsFromVersionId` is a LOCAL row id: it means nothing outside this + * database, differs on every mirror of the same collection, and describes an + * internal sharing optimization. The detail endpoints spread the whole version + * row into their response, so without this it would become a field clients could + * read and start depending on. + */ +function stripInternalVersionColumns(version: Record): Record { + const out = { ...version } + delete out.recordsFromVersionId + return out +} + /** * Strip owner-only data from a version row before returning it to a non-owner: * private-type entries in `typeCounts` (which would disclose the existence and @@ -291,9 +310,9 @@ const app = new Hono() ? Object.fromEntries(schemaEntries.map((e) => [e.slug, e.schema])) : filterSchemasForPublic(schemaEntries) - const versionView = ownerAccess - ? version - : sanitizeVersionForPublic(version, getPrivateTypes(schemaEntries)) + const versionView = stripInternalVersionColumns( + ownerAccess ? version : sanitizeVersionForPublic(version, getPrivateTypes(schemaEntries)), + ) return c.json({ ...versionView, @@ -347,9 +366,9 @@ const app = new Hono() ? Object.fromEntries(schemaEntries.map((e) => [e.slug, e.schema])) : filterSchemasForPublic(schemaEntries) - const versionView = ownerAccess - ? version - : sanitizeVersionForPublic(version, getPrivateTypes(schemaEntries)) + const versionView = stripInternalVersionColumns( + ownerAccess ? version : sanitizeVersionForPublic(version, getPrivateTypes(schemaEntries)), + ) return c.json({ ...versionView, @@ -406,7 +425,7 @@ const app = new Hono() // denormalized record_id + type and is indexed on // (version_id, [type,] record_id). record_objects is joined only to // fetch bodies for the page that survives the index scan. - const conditions = [eq(schema.versionRecords.versionId, version.id)] + const conditions = [eq(schema.versionRecords.versionId, recordsVersionId(version))] if (type) conditions.push(eq(schema.versionRecords.type, type)) // Cursor-based pagination: ?after=recordId (keyset pagination) @@ -592,7 +611,11 @@ const app = new Hono() const { semver } = parseSemver(n) const [version] = await db - .select({ id: schema.versions.id, recordCount: schema.versions.recordCount }) + .select({ + id: schema.versions.id, + recordCount: schema.versions.recordCount, + recordsFromVersionId: schema.versions.recordsFromVersionId, + }) .from(schema.versions) .where( and( @@ -638,7 +661,7 @@ const app = new Hono() // count is negligible next to streaming the rows. let streamedRecordCount = version.recordCount if (!ownerAccess || type) { - const countConditions = [eq(schema.versionRecords.versionId, version.id)] + const countConditions = [eq(schema.versionRecords.versionId, recordsVersionId(version))] if (type) countConditions.push(eq(schema.versionRecords.type, type)) if (!ownerAccess) { countConditions.push(eq(schema.versionRecords.private, false)) @@ -711,7 +734,7 @@ const app = new Hono() ${ownerAccess ? client`ro.hash` : client`coalesce(vr.public_record_hash, ro.hash)`} AS hash FROM version_records vr INNER JOIN record_objects ro ON ro.hash = vr.record_hash - WHERE vr.version_id = ${version.id} + WHERE vr.version_id = ${recordsVersionId(version)} ${type ? client`AND vr.type = ${type}` : client``} ${keyset} ${ownerAccess ? client`` : client`AND vr.private = false AND vr.type <> ALL(${privateTypeList}::text[])`} @@ -830,7 +853,7 @@ const app = new Hono() // Only load records that contain $file references (DB-level filter). // Non-owners never see records flagged private. const refConditions = [ - eq(schema.versionRecords.versionId, version.id), + eq(schema.versionRecords.versionId, recordsVersionId(version)), sql`${schema.recordObjects.data}::text LIKE '%"$file"%'`, ] if (!ownerAccess) refConditions.push(eq(schema.versionRecords.private, false)) @@ -958,7 +981,10 @@ const app = new Hono() const { semver: sinceSemver } = parseSemver(sinceParam) const [sinceVersion] = await db - .select({ id: schema.versions.id }) + .select({ + id: schema.versions.id, + recordsFromVersionId: schema.versions.recordsFromVersionId, + }) .from(schema.versions) .where( and( @@ -972,8 +998,12 @@ const app = new Hono() if (!sinceVersion) return c.json({ error: `Version ${sinceSemver} not found`, statusCode: 404 }, 404) - const targetId = version.id + // Record-set ids for the delta queries. `sinceId` stays the real version + // id because it also loads `version_schemas`, which a metadata patch owns + // outright — only the `version_records` side resolves through the pointer. + const targetId = recordsVersionId(version) const sinceId = sinceVersion.id + const sinceRecordsId = recordsVersionId(sinceVersion) const at = decodeDeltaCursor(cursor) type DeltaRow = { id: string; type: string; hash: string; recordHash: string } @@ -996,11 +1026,11 @@ const app = new Hono() : sql`` const previousServedHash = ownerAccess ? sql`(SELECT s.record_hash FROM version_records s - WHERE s.version_id = ${sinceId} AND s.record_id = vr.record_id LIMIT 1)` + WHERE s.version_id = ${sinceRecordsId} AND s.record_id = vr.record_id LIMIT 1)` : sql`(SELECT CASE WHEN s.private = false ${sinceTypeGuard} THEN coalesce(s.public_record_hash, s.record_hash) END FROM version_records s - WHERE s.version_id = ${sinceId} AND s.record_id = vr.record_id LIMIT 1)` + WHERE s.version_id = ${sinceRecordsId} AND s.record_id = vr.record_id LIMIT 1)` // Visibility of a row for the CALLER, evaluated against the version that // row belongs to (private-type sets differ between versions). Applied to @@ -1059,12 +1089,19 @@ const app = new Hono() return Promise.all([ run( at.added, - deltaQuery(targetId, privateTypes, sinceId, sincePrivateTypes, 'absent', at.added), + deltaQuery( + targetId, + privateTypes, + sinceRecordsId, + sincePrivateTypes, + 'absent', + at.added, + ), ), run( at.removed, deltaQuery( - sinceId, + sinceRecordsId, sincePrivateTypes, targetId, privateTypes, @@ -1077,7 +1114,7 @@ const app = new Hono() deltaQuery( targetId, privateTypes, - sinceId, + sinceRecordsId, sincePrivateTypes, 'changed', at.updated, @@ -1131,7 +1168,7 @@ const app = new Hono() SELECT vr.record_id AS id, vr.type, ${servedHash} AS hash, vr.record_hash AS "recordHash", vr.private FROM version_records vr - WHERE vr.version_id = ${version.id} + WHERE vr.version_id = ${recordsVersionId(version)} ${privacyWhere} ${afterCursor('vr', at.added)} ORDER BY vr.record_id, vr.record_hash LIMIT ${limit + 1} @@ -1204,7 +1241,7 @@ const app = new Hono() return c.json({ error: 'Version not found', statusCode: 404 }, 404) } - const targetId = targetVersion.id + const targetId = recordsVersionId(targetVersion) let fromVersion: typeof targetVersion | null = null if (from) { const { semver: fromSemver } = parseSemver(from) @@ -1224,6 +1261,9 @@ const app = new Hono() } const fromId = fromVersion?.id + // As in the delta path: `fromId` loads schemas, `fromRecordsId` reads + // `version_records`. They differ only when `from` is a metadata patch. + const fromRecordsId = fromVersion ? recordsVersionId(fromVersion) : undefined // Privacy filtering for non-owners: hide private types and private records. // record_objects is joined only for the record body (ro.data); record-level @@ -1300,17 +1340,17 @@ const app = new Hono() diffQuery( targetId, privateTypes, - fromId, + fromRecordsId, fromPrivateTypes, - fromId ? 'absent' : 'all', + fromRecordsId ? 'absent' : 'all', diffCursor.added, ), ), run( diffCursor.removed, - fromId + fromRecordsId ? diffQuery( - fromId, + fromRecordsId, fromPrivateTypes, targetId, privateTypes, @@ -1321,11 +1361,11 @@ const app = new Hono() ), run( diffCursor.updated, - fromId + fromRecordsId ? diffQuery( targetId, privateTypes, - fromId, + fromRecordsId, fromPrivateTypes, 'changed', diffCursor.updated, @@ -1425,6 +1465,12 @@ const app = new Hono() openApi({ tags: ['Versions'], summary: 'Update collection metadata, creating a new patch version', + description: + 'Creates a patch version carrying the merged metadata. Building it means folding both ' + + 'version digests over the record set and copying every `version_records` row, which on a ' + + 'multi-million-record collection takes longer than an HTTP request survives. Pass ' + + '`?async=true` to get a `202` with a `job_id` and poll ' + + '`GET /:owner/:slug/metadata/jobs/:jobId` for the outcome.', request: { param: z.object({ owner: z.string(), slug: z.string() }), // Metadata is a free-form JSON object (readme, description, license, ...) @@ -1436,6 +1482,12 @@ const app = new Hono() const { owner, slug } = c.req.valid('param') const body = c.req.valid('json') + // Query param only, unlike the negotiate commit which also accepts + // `{async: true}` in the body. Here the body *is* the metadata, so an + // `async` key in it would be merged into the stored metadata and persisted. + const asyncQuery = c.req.query('async') + const wantsAsync = asyncQuery === 'true' || asyncQuery === '1' + // Metadata is hashed and stored on every version row — keep it bounded if (JSON.stringify(body).length > MAX_METADATA_BYTES) { return c.json( @@ -1475,141 +1527,285 @@ const app = new Hono() return c.json({ semver: latest.semver, unchanged: true }) } - const schemaEntries = await loadVersionSchemas(latest.id) - const schemaSet = schemaEntries.map((e) => ({ slug: e.slug, schemaHash: e.schemaHash })) - // Hash-only load: public hashes were computed and stored at commit time, - // so a metadata-only version never needs the record bodies - const fileHashes = ( - await db - .select({ hash: schema.versionFiles.fileHash }) - .from(schema.versionFiles) - .where(eq(schema.versionFiles.versionId, latest.id)) - ).map((f) => f.hash) - - const privateTypes = getPrivateTypes(schemaEntries) - const publicSchemaSet = schemaEntries - .filter((e) => !privateTypes.has(e.slug)) - .map((e) => ({ slug: e.slug, schemaHash: hashSchema(filterTypeSchema(e.schema)) })) - - // Both digests are folded over hashes streamed from Postgres in sorted - // order, exactly as the commit path does. Loading every row to build two - // in-memory arrays made a metadata edit cost as much as a full push — on a - // multi-million-record collection, several hundred MB of JS objects to - // change a description. - // - // COLLATE "C" is required: the digest must see byte order, which is what - // Array.prototype.sort() produces, not the database's locale collation. - const client = db.$client - const CURSOR_CHUNK = 10_000 + // Two writers both reading `latest` would derive the same patch semver and + // one would lose to the (collection_id, semver) unique constraint after + // doing all the work. Refuse up front instead — and refuse for the + // synchronous path too, since a sync PATCH races a running job just as + // badly. + const [inFlight] = await db + .select({ id: schema.metadataJobs.id }) + .from(schema.metadataJobs) + .where( + and( + eq(schema.metadataJobs.collectionId, collection.id), + eq(schema.metadataJobs.status, 'running'), + ), + ) + .limit(1) + if (inFlight) { + return c.json( + { + error: 'A metadata update is already in progress for this collection', + statusCode: 409, + job_id: inFlight.id, + }, + 409, + ) + } - const versionHashStream = new VersionHashStream(schemaSet, fileHashes, newMetadata) - await client` + const buildVersion = async (): Promise<{ + status: ContentfulStatusCode + body: Record + }> => { + const schemaEntries = await loadVersionSchemas(latest.id) + const schemaSet = schemaEntries.map((e) => ({ slug: e.slug, schemaHash: e.schemaHash })) + // Hash-only load: public hashes were computed and stored at commit time, + // so a metadata-only version never needs the record bodies + const fileHashes = ( + await db + .select({ hash: schema.versionFiles.fileHash }) + .from(schema.versionFiles) + .where(eq(schema.versionFiles.versionId, latest.id)) + ).map((f) => f.hash) + + const privateTypes = getPrivateTypes(schemaEntries) + const publicSchemaSet = schemaEntries + .filter((e) => !privateTypes.has(e.slug)) + .map((e) => ({ slug: e.slug, schemaHash: hashSchema(filterTypeSchema(e.schema)) })) + + // Both digests are folded over hashes streamed from Postgres in sorted + // order, exactly as the commit path does. Loading every row to build two + // in-memory arrays made a metadata edit cost as much as a full push — on a + // multi-million-record collection, several hundred MB of JS objects to + // change a description. + // + // COLLATE "C" is required: the digest must see byte order, which is what + // Array.prototype.sort() produces, not the database's locale collation. + const client = db.$client + const CURSOR_CHUNK = 10_000 + + const versionHashStream = new VersionHashStream(schemaSet, fileHashes, newMetadata) + await client` SELECT record_hash AS h FROM version_records - WHERE version_id = ${latest.id} + WHERE version_id = ${recordsVersionId(latest)} ORDER BY record_hash COLLATE "C" `.cursor(CURSOR_CHUNK, (rows) => { - for (const row of rows) versionHashStream.push(row['h'] as string) - }) - const versionHash = versionHashStream.digest() + for (const row of rows) versionHashStream.push(row['h'] as string) + }) + const versionHash = versionHashStream.digest() - const publicHashStream = new VersionHashStream(publicSchemaSet, fileHashes, newMetadata) - await client` + const publicHashStream = new VersionHashStream(publicSchemaSet, fileHashes, newMetadata) + await client` SELECT coalesce(vr.public_record_hash, vr.record_hash) AS h FROM version_records vr - WHERE vr.version_id = ${latest.id} + WHERE vr.version_id = ${recordsVersionId(latest)} AND NOT vr.private AND vr.type <> ALL(${[...privateTypes]}::text[]) ORDER BY coalesce(vr.public_record_hash, vr.record_hash) COLLATE "C" `.cursor(CURSOR_CHUNK, (rows) => { - for (const row of rows) publicHashStream.push(row['h'] as string) - }) - const publicHash = publicHashStream.digest().replace('private:', 'public:') - - const sv = deriveSemver(latest.semver, false, false, true) - - let newVersionId: number | undefined - await db.transaction(async (tx) => { - const [version] = await tx - .insert(schema.versions) - .values({ - collectionId: collection.id, - semver: sv.semver, - major: sv.major, - minor: sv.minor, - patch: sv.patch, - hash: versionHash, - publicHash, - baseSemver: latest.semver, - message: `Update metadata`, - metadata: newMetadata, - pushedBy: userId ?? null, - recordCount: latest.recordCount, - fileCount: latest.fileCount, - // Same record set as the base version, so the per-type counts carry - // over unchanged. - typeCounts: latest.typeCounts, - totalBytes: latest.totalBytes, - }) - .returning({ id: schema.versions.id }) + for (const row of rows) publicHashStream.push(row['h'] as string) + }) + const publicHash = publicHashStream.digest().replace('private:', 'public:') + + const sv = deriveSemver(latest.semver, false, false, true) + + let newVersionId: number | undefined + await db.transaction(async (tx) => { + const [version] = await tx + .insert(schema.versions) + .values({ + collectionId: collection.id, + semver: sv.semver, + major: sv.major, + minor: sv.minor, + patch: sv.patch, + hash: versionHash, + publicHash, + baseSemver: latest.semver, + message: `Update metadata`, + metadata: newMetadata, + pushedBy: userId ?? null, + recordCount: latest.recordCount, + fileCount: latest.fileCount, + // Same record set as the base version, so the per-type counts carry + // over unchanged. + typeCounts: latest.typeCounts, + totalBytes: latest.totalBytes, + // Share the base's record set rather than copying it. Points at the + // version that actually owns the rows: if the base is itself a + // metadata patch, inherit its pointer so this stays one hop. + recordsFromVersionId: recordsVersionId(latest), + }) + .returning({ id: schema.versions.id }) + + newVersionId = version!.id + + if (schemaEntries.length > 0) { + await tx.insert(schema.versionSchemas).values( + schemaEntries.map((e) => ({ + versionId: version!.id, + slug: e.slug, + schemaId: e.schemaId, + })), + ) + } - newVersionId = version!.id + // No record-set copy: `recordsFromVersionId` above points at the base's + // rows. The schema set is unchanged, so every column that copy used to + // carry — the public content-addresses and the per-version `private` + // flag included — is now read from the base directly, which cannot + // drift from it by construction. - if (schemaEntries.length > 0) { - await tx.insert(schema.versionSchemas).values( - schemaEntries.map((e) => ({ - versionId: version!.id, - slug: e.slug, - schemaId: e.schemaId, - })), + if (fileHashes.length > 0) { + await tx + .insert(schema.versionFiles) + .values(fileHashes.map((h) => ({ versionId: version!.id, fileHash: h }))) + } + + await tx + .update(schema.collections) + .set({ updatedAt: new Date() }) + .where(eq(schema.collections.id, collection.id)) + }) + + // Fire webhooks for the metadata (patch) version — best-effort. + try { + const deliveryIds = await enqueueWebhookDeliveries( + { + id: newVersionId!, + semver: sv.semver, + hash: versionHash, + major: sv.major, + minor: sv.minor, + patch: sv.patch, + recordCount: latest.recordCount, + fileCount: latest.fileCount, + }, + 'patch', + collection.id, ) + dispatchDeliveries(deliveryIds) + } catch (err) { + console.error(`[webhooks] failed to enqueue for ${sv.semver}:`, err) } - // Copy the record set server-side. The schema set is unchanged, so every - // column — including the public content-addresses and the per-version - // `private` flag — carries over as-is. `private` MUST be copied: it - // defaults to false, so omitting it would silently de-privatize every - // private record in the new (and now latest) version. - await tx.execute(sql` - INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type, private) - SELECT ${version!.id}, record_hash, public_record_hash, record_id, type, private - FROM version_records - WHERE version_id = ${latest.id} - `) - - if (fileHashes.length > 0) { - await tx - .insert(schema.versionFiles) - .values(fileHashes.map((h) => ({ versionId: version!.id, fileHash: h }))) + return { + status: 201, + body: { semver: sv.semver, hash: versionHash, metadata: newMetadata }, } + } - await tx - .update(schema.collections) - .set({ updatedAt: new Date() }) - .where(eq(schema.collections.id, collection.id)) - }) + // Synchronous by default: the CLI, existing scripts and every caller + // written before `?async=true` expect the version in the response, and on + // an ordinary collection the whole thing takes well under a second. + if (!wantsAsync) { + const { status, body } = await buildVersion() + return c.json(body, status) + } - // Fire webhooks for the metadata (patch) version — best-effort. - try { - const deliveryIds = await enqueueWebhookDeliveries( - { - id: newVersionId!, - semver: sv.semver, - hash: versionHash, - major: sv.major, - minor: sv.minor, - patch: sv.patch, - recordCount: latest.recordCount, - fileCount: latest.fileCount, - }, - 'patch', - collection.id, - ) - dispatchDeliveries(deliveryIds) - } catch (err) { - console.error(`[webhooks] failed to enqueue for ${sv.semver}:`, err) + const [job] = await db + .insert(schema.metadataJobs) + .values({ + collectionId: collection.id, + userId: userId ?? null, + baseSemver: latest.semver, + metadata: newMetadata, + }) + .returning({ id: schema.metadataJobs.id }) + + // Deliberately not awaited: the response goes out now and the outcome is + // recorded on the job for the client to poll. A process that dies mid-build + // rolls the version transaction back and leaves the job 'running', which + // the cleanup sweep fails out. + void (async () => { + const startedAt = Date.now() + try { + const { status, body } = await buildVersion() + const ok = status >= 200 && status < 300 + await db + .update(schema.metadataJobs) + .set({ + status: ok ? 'completed' : 'failed', + ...(ok ? { result: body as never } : { error: body as never }), + finishedAt: new Date(), + }) + .where(eq(schema.metadataJobs.id, job!.id)) + console.log( + `[metadata] async job ${job!.id} ${ok ? `created ${body['semver']}` : `failed (${status})`} in ${Math.round((Date.now() - startedAt) / 1000)}s`, + ) + } catch (err) { + console.error(`[metadata] async job ${job!.id} threw: ${describeError(err)}`, err) + await db + .update(schema.metadataJobs) + .set({ + status: 'failed', + error: { statusCode: 500, error: describeError(err) }, + finishedAt: new Date(), + }) + .where(eq(schema.metadataJobs.id, job!.id)) + .catch(() => {}) + } + })() + + return c.json({ job_id: job!.id, status: 'running', base_semver: latest.semver }, 202) + }, + ) + // Poll an async metadata job started with `PATCH …/metadata?async=true` + .get( + '/:owner/:slug/metadata/jobs/:jobId', + requireAuth('read'), + openApi({ + tags: ['Versions'], + summary: 'Get the status of an async metadata update', + description: + '`status` is one of `running`, `completed` or `failed`. `result` holds the created ' + + 'version once status is `completed`, and `error` the rejection body once it is `failed`.', + request: { + param: z.object({ owner: z.string(), slug: z.string(), jobId: z.string() }), + }, + responses: { 200: z.any() }, + }), + async (c) => { + const { owner, slug, jobId } = c.req.valid('param') + + const collection = await resolveCollection(owner, slug) + if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) + + // Authorized by collection access rather than by who started the job: a + // job is a property of the collection, and anyone who could write the + // metadata can see how the write went. + const userId = c.get('userId') + if (!(await hasOrgAccess(userId, collection.organizationId))) { + return c.json({ error: 'Forbidden', statusCode: 403 }, 403) } - return c.json({ semver: sv.semver, hash: versionHash, metadata: newMetadata }, 201) + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(collection.id)) { + return c.json({ error: 'API key is not scoped to this collection', statusCode: 403 }, 403) + } + + const [job] = await db + .select() + .from(schema.metadataJobs) + .where( + and( + eq(schema.metadataJobs.id, jobId), + eq(schema.metadataJobs.collectionId, collection.id), + ), + ) + .limit(1) + + if (!job) return c.json({ error: 'Job not found', statusCode: 404 }, 404) + + return c.json({ + job_id: job.id, + status: job.status, + base_semver: job.baseSemver, + started_at: job.startedAt, + finished_at: job.finishedAt, + result: job.result ?? null, + error: job.error ?? null, + }) }, ) @@ -1625,7 +1821,12 @@ const app = new Hono() * regardless of the filter, which was simply wrong under `?type=`. */ async function countVersionRecords( - version: { id: number; recordCount: number; typeCounts: Record | null }, + version: { + id: number + recordCount: number + typeCounts: Record | null + recordsFromVersionId: number | null + }, type: string | undefined, privateTypes: Set, ): Promise { @@ -1641,7 +1842,7 @@ async function countVersionRecords( if (!type && privateTypes.size === 0) return version.recordCount - const conditions = [eq(schema.versionRecords.versionId, version.id)] + const conditions = [eq(schema.versionRecords.versionId, recordsVersionId(version))] if (type) conditions.push(eq(schema.versionRecords.type, type)) for (const pt of privateTypes) conditions.push(sql`${schema.versionRecords.type} != ${pt}`) const [row] = await db diff --git a/src/db/migrations/0013_windy_blue_marvel.sql b/src/db/migrations/0013_windy_blue_marvel.sql new file mode 100644 index 0000000..0dc612e --- /dev/null +++ b/src/db/migrations/0013_windy_blue_marvel.sql @@ -0,0 +1,16 @@ +CREATE TABLE "metadata_jobs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "collection_id" uuid NOT NULL, + "user_id" text, + "status" text DEFAULT 'running' NOT NULL, + "base_semver" text NOT NULL, + "metadata" jsonb NOT NULL, + "result" jsonb, + "error" jsonb, + "started_at" timestamp with time zone DEFAULT now() NOT NULL, + "finished_at" timestamp with time zone +); +--> statement-breakpoint +ALTER TABLE "metadata_jobs" ADD CONSTRAINT "metadata_jobs_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collections"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "metadata_jobs" ADD CONSTRAINT "metadata_jobs_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "metadata_jobs_collection_status_idx" ON "metadata_jobs" USING btree ("collection_id","status"); \ No newline at end of file diff --git a/src/db/migrations/0014_fantastic_skrulls.sql b/src/db/migrations/0014_fantastic_skrulls.sql new file mode 100644 index 0000000..d44c8cc --- /dev/null +++ b/src/db/migrations/0014_fantastic_skrulls.sql @@ -0,0 +1,2 @@ +ALTER TABLE "versions" ADD COLUMN "records_from_version_id" bigint;--> statement-breakpoint +ALTER TABLE "versions" ADD CONSTRAINT "versions_records_from_version_id_versions_id_fk" FOREIGN KEY ("records_from_version_id") REFERENCES "public"."versions"("id") ON DELETE restrict ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/meta/0013_snapshot.json b/src/db/migrations/meta/0013_snapshot.json new file mode 100644 index 0000000..a052f3b --- /dev/null +++ b/src/db/migrations/meta/0013_snapshot.json @@ -0,0 +1,2863 @@ +{ + "id": "afc8e2d6-7e7c-412b-b6df-d176e45d943f", + "prevId": "50b02777-a57b-4c16-ad39-92a2162f818f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_collections": { + "name": "ark_collections", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "ark_id": { + "name": "ark_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_url": { + "name": "custom_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_collections_collection_id_collections_id_fk": { + "name": "ark_collections_collection_id_collections_id_fk", + "tableFrom": "ark_collections", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_collections_ark_id_unique": { + "name": "ark_collections_ark_id_unique", + "nullsNotDistinct": false, + "columns": ["ark_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_record_types": { + "name": "ark_record_types", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url_field": { + "name": "redirect_url_field", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ark_record_types_collection_id_collections_id_fk": { + "name": "ark_record_types_collection_id_collections_id_fk", + "tableFrom": "ark_record_types", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ark_record_types_collection_id_record_type_pk": { + "name": "ark_record_types_collection_id_record_type_pk", + "columns": ["collection_id", "record_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_shoulders": { + "name": "ark_shoulders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shoulder": { + "name": "shoulder", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_shoulders_organization_id_organization_id_fk": { + "name": "ark_shoulders_organization_id_organization_id_fk", + "tableFrom": "ark_shoulders", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_shoulders_organization_id_unique": { + "name": "ark_shoulders_organization_id_unique", + "nullsNotDistinct": false, + "columns": ["organization_id"] + }, + "ark_shoulders_shoulder_unique": { + "name": "ark_shoulders_shoulder_unique", + "nullsNotDistinct": false, + "columns": ["shoulder"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_webhooks": { + "name": "collection_webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bump_filter": { + "name": "bump_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{major,minor,patch}'::text[]" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_delivery_at": { + "name": "last_delivery_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "collection_webhooks_collection_id_idx": { + "name": "collection_webhooks_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collection_webhooks_collection_id_collections_id_fk": { + "name": "collection_webhooks_collection_id_collections_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_webhooks_created_by_user_id_fk": { + "name": "collection_webhooks_created_by_user_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collections_organization_id_idx": { + "name": "collections_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collections_organization_id_organization_id_fk": { + "name": "collections_organization_id_organization_id_fk", + "tableFrom": "collections", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collections_forked_from_collections_id_fk": { + "name": "collections_forked_from_collections_id_fk", + "tableFrom": "collections", + "tableTo": "collections", + "columnsFrom": ["forked_from"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "collections_organization_id_slug_unique": { + "name": "collections_organization_id_slug_unique", + "nullsNotDistinct": false, + "columns": ["organization_id", "slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata_jobs": { + "name": "metadata_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "metadata_jobs_collection_status_idx": { + "name": "metadata_jobs_collection_status_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_jobs_collection_id_collections_id_fk": { + "name": "metadata_jobs_collection_id_collections_id_fk", + "tableFrom": "metadata_jobs", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "metadata_jobs_user_id_user_id_fk": { + "name": "metadata_jobs_user_id_user_id_fk", + "tableFrom": "metadata_jobs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_session_manifest": { + "name": "negotiate_session_manifest", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "needed": { + "name": "needed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "submitted": { + "name": "submitted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "final_hash": { + "name": "final_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "nsm_session_needed_idx": { + "name": "nsm_session_needed_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "needed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "negotiate_session_manifest_session_id_negotiate_sessions_id_fk": { + "name": "negotiate_session_manifest_session_id_negotiate_sessions_id_fk", + "tableFrom": "negotiate_session_manifest", + "tableTo": "negotiate_sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "negotiate_session_manifest_session_id_hash_pk": { + "name": "negotiate_session_manifest_session_id_hash_pk", + "columns": ["session_id", "hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_sessions": { + "name": "negotiate_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schemas": { + "name": "schemas", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "file_hashes": { + "name": "file_hashes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "needed_files": { + "name": "needed_files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "strip_unknown_fields": { + "name": "strip_unknown_fields", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_expected": { + "name": "manifest_expected", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finalize_started_at": { + "name": "finalize_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "negotiate_sessions_collection_id_collections_id_fk": { + "name": "negotiate_sessions_collection_id_collections_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "negotiate_sessions_user_id_user_id_fk": { + "name": "negotiate_sessions_user_id_user_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ark_naan": { + "name": "ark_naan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kf_org_id": { + "name": "kf_org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_comments": { + "name": "page_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page": { + "name": "page", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quote": { + "name": "quote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quote_context": { + "name": "quote_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_comments_page_anchor_idx": { + "name": "page_comments_page_anchor_idx", + "columns": [ + { + "expression": "page", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anchor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_comments_user_id_idx": { + "name": "page_comments_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_comments_user_id_user_id_fk": { + "name": "page_comments_user_id_user_id_fk", + "tableFrom": "page_comments", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.record_objects": { + "name": "record_objects", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "record_objects_record_id_idx": { + "name": "record_objects_record_id_idx", + "columns": [ + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schema_labels": { + "name": "schema_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schema_labels_label_idx": { + "name": "schema_labels_label_idx", + "columns": [ + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schema_labels_schema_id_schemas_id_fk": { + "name": "schema_labels_schema_id_schemas_id_fk", + "tableFrom": "schema_labels", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schema_labels_schema_id_label_unique": { + "name": "schema_labels_schema_id_label_unique", + "nullsNotDistinct": false, + "columns": ["schema_id", "label"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schemas": { + "name": "schemas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schemas_schema_hash_unique": { + "name": "schemas_schema_hash_unique", + "nullsNotDistinct": false, + "columns": ["schema_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_runs": { + "name": "sync_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "collections_synced": { + "name": "collections_synced", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_created": { + "name": "collections_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_failed": { + "name": "collections_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "versions_pulled": { + "name": "versions_pulled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_downloaded": { + "name": "files_downloaded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_skipped": { + "name": "files_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_files": { + "name": "version_files", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_files_file_hash_idx": { + "name": "version_files_file_hash_idx", + "columns": [ + { + "expression": "file_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_files_version_id_versions_id_fk": { + "name": "version_files_version_id_versions_id_fk", + "tableFrom": "version_files", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_files_file_hash_files_hash_fk": { + "name": "version_files_file_hash_files_hash_fk", + "tableFrom": "version_files", + "tableTo": "files", + "columnsFrom": ["file_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_files_version_id_file_hash_pk": { + "name": "version_files_version_id_file_hash_pk", + "columns": ["version_id", "file_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_records": { + "name": "version_records", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "record_hash": { + "name": "record_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_record_hash": { + "name": "public_record_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_records_record_hash_idx": { + "name": "version_records_record_hash_idx", + "columns": [ + { + "expression": "record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_public_record_hash_idx": { + "name": "version_records_public_record_hash_idx", + "columns": [ + { + "expression": "public_record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "public_record_hash IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_record_idx": { + "name": "version_records_version_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_type_record_idx": { + "name": "version_records_version_type_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_records_version_id_versions_id_fk": { + "name": "version_records_version_id_versions_id_fk", + "tableFrom": "version_records", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_records_record_hash_record_objects_hash_fk": { + "name": "version_records_record_hash_record_objects_hash_fk", + "tableFrom": "version_records", + "tableTo": "record_objects", + "columnsFrom": ["record_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_records_version_id_record_hash_pk": { + "name": "version_records_version_id_record_hash_pk", + "columns": ["version_id", "record_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_schemas": { + "name": "version_schemas", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_schemas_schema_id_idx": { + "name": "version_schemas_schema_id_idx", + "columns": [ + { + "expression": "schema_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_schemas_version_id_versions_id_fk": { + "name": "version_schemas_version_id_versions_id_fk", + "tableFrom": "version_schemas", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_schemas_schema_id_schemas_id_fk": { + "name": "version_schemas_schema_id_schemas_id_fk", + "tableFrom": "version_schemas", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_schemas_version_id_slug_pk": { + "name": "version_schemas_version_id_slug_pk", + "columns": ["version_id", "slug"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.versions": { + "name": "versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pushed_by": { + "name": "pushed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "record_count": { + "name": "record_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_counts": { + "name": "type_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "total_bytes": { + "name": "total_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "versions_ordering_idx": { + "name": "versions_ordering_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "versions_collection_id_collections_id_fk": { + "name": "versions_collection_id_collections_id_fk", + "tableFrom": "versions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "versions_pushed_by_user_id_fk": { + "name": "versions_pushed_by_user_id_fk", + "tableFrom": "versions", + "tableTo": "user", + "columnsFrom": ["pushed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "versions_collection_id_semver_unique": { + "name": "versions_collection_id_semver_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "semver"] + }, + "versions_collection_id_hash_public_hash_unique": { + "name": "versions_collection_id_hash_public_hash_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "hash", "public_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bump_type": { + "name": "bump_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'version.created'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "response_code": { + "name": "response_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_webhook_id_idx": { + "name": "webhook_deliveries_webhook_id_idx", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_collection_id_idx": { + "name": "webhook_deliveries_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_created_at_idx": { + "name": "webhook_deliveries_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_sweep_idx": { + "name": "webhook_deliveries_sweep_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_collection_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_collection_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collection_webhooks", + "columnsFrom": ["webhook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_collection_id_collections_id_fk": { + "name": "webhook_deliveries_collection_id_collections_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_version_id_versions_id_fk": { + "name": "webhook_deliveries_version_id_versions_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/0014_snapshot.json b/src/db/migrations/meta/0014_snapshot.json new file mode 100644 index 0000000..36a2fde --- /dev/null +++ b/src/db/migrations/meta/0014_snapshot.json @@ -0,0 +1,2878 @@ +{ + "id": "e459b9cf-35df-4110-9bd6-25214e4616bd", + "prevId": "afc8e2d6-7e7c-412b-b6df-d176e45d943f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_collections": { + "name": "ark_collections", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "ark_id": { + "name": "ark_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_url": { + "name": "custom_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_collections_collection_id_collections_id_fk": { + "name": "ark_collections_collection_id_collections_id_fk", + "tableFrom": "ark_collections", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_collections_ark_id_unique": { + "name": "ark_collections_ark_id_unique", + "nullsNotDistinct": false, + "columns": ["ark_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_record_types": { + "name": "ark_record_types", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url_field": { + "name": "redirect_url_field", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ark_record_types_collection_id_collections_id_fk": { + "name": "ark_record_types_collection_id_collections_id_fk", + "tableFrom": "ark_record_types", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ark_record_types_collection_id_record_type_pk": { + "name": "ark_record_types_collection_id_record_type_pk", + "columns": ["collection_id", "record_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_shoulders": { + "name": "ark_shoulders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shoulder": { + "name": "shoulder", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_shoulders_organization_id_organization_id_fk": { + "name": "ark_shoulders_organization_id_organization_id_fk", + "tableFrom": "ark_shoulders", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_shoulders_organization_id_unique": { + "name": "ark_shoulders_organization_id_unique", + "nullsNotDistinct": false, + "columns": ["organization_id"] + }, + "ark_shoulders_shoulder_unique": { + "name": "ark_shoulders_shoulder_unique", + "nullsNotDistinct": false, + "columns": ["shoulder"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_webhooks": { + "name": "collection_webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bump_filter": { + "name": "bump_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{major,minor,patch}'::text[]" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_delivery_at": { + "name": "last_delivery_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "collection_webhooks_collection_id_idx": { + "name": "collection_webhooks_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collection_webhooks_collection_id_collections_id_fk": { + "name": "collection_webhooks_collection_id_collections_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_webhooks_created_by_user_id_fk": { + "name": "collection_webhooks_created_by_user_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collections_organization_id_idx": { + "name": "collections_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collections_organization_id_organization_id_fk": { + "name": "collections_organization_id_organization_id_fk", + "tableFrom": "collections", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collections_forked_from_collections_id_fk": { + "name": "collections_forked_from_collections_id_fk", + "tableFrom": "collections", + "tableTo": "collections", + "columnsFrom": ["forked_from"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "collections_organization_id_slug_unique": { + "name": "collections_organization_id_slug_unique", + "nullsNotDistinct": false, + "columns": ["organization_id", "slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata_jobs": { + "name": "metadata_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "metadata_jobs_collection_status_idx": { + "name": "metadata_jobs_collection_status_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_jobs_collection_id_collections_id_fk": { + "name": "metadata_jobs_collection_id_collections_id_fk", + "tableFrom": "metadata_jobs", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "metadata_jobs_user_id_user_id_fk": { + "name": "metadata_jobs_user_id_user_id_fk", + "tableFrom": "metadata_jobs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_session_manifest": { + "name": "negotiate_session_manifest", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "needed": { + "name": "needed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "submitted": { + "name": "submitted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "final_hash": { + "name": "final_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "nsm_session_needed_idx": { + "name": "nsm_session_needed_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "needed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "negotiate_session_manifest_session_id_negotiate_sessions_id_fk": { + "name": "negotiate_session_manifest_session_id_negotiate_sessions_id_fk", + "tableFrom": "negotiate_session_manifest", + "tableTo": "negotiate_sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "negotiate_session_manifest_session_id_hash_pk": { + "name": "negotiate_session_manifest_session_id_hash_pk", + "columns": ["session_id", "hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_sessions": { + "name": "negotiate_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schemas": { + "name": "schemas", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "file_hashes": { + "name": "file_hashes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "needed_files": { + "name": "needed_files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "strip_unknown_fields": { + "name": "strip_unknown_fields", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_expected": { + "name": "manifest_expected", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finalize_started_at": { + "name": "finalize_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "negotiate_sessions_collection_id_collections_id_fk": { + "name": "negotiate_sessions_collection_id_collections_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "negotiate_sessions_user_id_user_id_fk": { + "name": "negotiate_sessions_user_id_user_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ark_naan": { + "name": "ark_naan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kf_org_id": { + "name": "kf_org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_comments": { + "name": "page_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page": { + "name": "page", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quote": { + "name": "quote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quote_context": { + "name": "quote_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_comments_page_anchor_idx": { + "name": "page_comments_page_anchor_idx", + "columns": [ + { + "expression": "page", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anchor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_comments_user_id_idx": { + "name": "page_comments_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_comments_user_id_user_id_fk": { + "name": "page_comments_user_id_user_id_fk", + "tableFrom": "page_comments", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.record_objects": { + "name": "record_objects", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "record_objects_record_id_idx": { + "name": "record_objects_record_id_idx", + "columns": [ + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schema_labels": { + "name": "schema_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schema_labels_label_idx": { + "name": "schema_labels_label_idx", + "columns": [ + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schema_labels_schema_id_schemas_id_fk": { + "name": "schema_labels_schema_id_schemas_id_fk", + "tableFrom": "schema_labels", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schema_labels_schema_id_label_unique": { + "name": "schema_labels_schema_id_label_unique", + "nullsNotDistinct": false, + "columns": ["schema_id", "label"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schemas": { + "name": "schemas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schemas_schema_hash_unique": { + "name": "schemas_schema_hash_unique", + "nullsNotDistinct": false, + "columns": ["schema_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_runs": { + "name": "sync_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "collections_synced": { + "name": "collections_synced", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_created": { + "name": "collections_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_failed": { + "name": "collections_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "versions_pulled": { + "name": "versions_pulled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_downloaded": { + "name": "files_downloaded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_skipped": { + "name": "files_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_files": { + "name": "version_files", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_files_file_hash_idx": { + "name": "version_files_file_hash_idx", + "columns": [ + { + "expression": "file_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_files_version_id_versions_id_fk": { + "name": "version_files_version_id_versions_id_fk", + "tableFrom": "version_files", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_files_file_hash_files_hash_fk": { + "name": "version_files_file_hash_files_hash_fk", + "tableFrom": "version_files", + "tableTo": "files", + "columnsFrom": ["file_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_files_version_id_file_hash_pk": { + "name": "version_files_version_id_file_hash_pk", + "columns": ["version_id", "file_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_records": { + "name": "version_records", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "record_hash": { + "name": "record_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_record_hash": { + "name": "public_record_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_records_record_hash_idx": { + "name": "version_records_record_hash_idx", + "columns": [ + { + "expression": "record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_public_record_hash_idx": { + "name": "version_records_public_record_hash_idx", + "columns": [ + { + "expression": "public_record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "public_record_hash IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_record_idx": { + "name": "version_records_version_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_type_record_idx": { + "name": "version_records_version_type_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_records_version_id_versions_id_fk": { + "name": "version_records_version_id_versions_id_fk", + "tableFrom": "version_records", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_records_record_hash_record_objects_hash_fk": { + "name": "version_records_record_hash_record_objects_hash_fk", + "tableFrom": "version_records", + "tableTo": "record_objects", + "columnsFrom": ["record_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_records_version_id_record_hash_pk": { + "name": "version_records_version_id_record_hash_pk", + "columns": ["version_id", "record_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_schemas": { + "name": "version_schemas", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_schemas_schema_id_idx": { + "name": "version_schemas_schema_id_idx", + "columns": [ + { + "expression": "schema_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_schemas_version_id_versions_id_fk": { + "name": "version_schemas_version_id_versions_id_fk", + "tableFrom": "version_schemas", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_schemas_schema_id_schemas_id_fk": { + "name": "version_schemas_schema_id_schemas_id_fk", + "tableFrom": "version_schemas", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_schemas_version_id_slug_pk": { + "name": "version_schemas_version_id_slug_pk", + "columns": ["version_id", "slug"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.versions": { + "name": "versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pushed_by": { + "name": "pushed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "record_count": { + "name": "record_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_counts": { + "name": "type_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "total_bytes": { + "name": "total_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "records_from_version_id": { + "name": "records_from_version_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "versions_ordering_idx": { + "name": "versions_ordering_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "versions_collection_id_collections_id_fk": { + "name": "versions_collection_id_collections_id_fk", + "tableFrom": "versions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "versions_pushed_by_user_id_fk": { + "name": "versions_pushed_by_user_id_fk", + "tableFrom": "versions", + "tableTo": "user", + "columnsFrom": ["pushed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "versions_records_from_version_id_versions_id_fk": { + "name": "versions_records_from_version_id_versions_id_fk", + "tableFrom": "versions", + "tableTo": "versions", + "columnsFrom": ["records_from_version_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "versions_collection_id_semver_unique": { + "name": "versions_collection_id_semver_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "semver"] + }, + "versions_collection_id_hash_public_hash_unique": { + "name": "versions_collection_id_hash_public_hash_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "hash", "public_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bump_type": { + "name": "bump_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'version.created'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "response_code": { + "name": "response_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_webhook_id_idx": { + "name": "webhook_deliveries_webhook_id_idx", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_collection_id_idx": { + "name": "webhook_deliveries_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_created_at_idx": { + "name": "webhook_deliveries_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_sweep_idx": { + "name": "webhook_deliveries_sweep_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_collection_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_collection_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collection_webhooks", + "columnsFrom": ["webhook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_collection_id_collections_id_fk": { + "name": "webhook_deliveries_collection_id_collections_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_version_id_versions_id_fk": { + "name": "webhook_deliveries_version_id_versions_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 04ded63..1dde5b0 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -92,6 +92,20 @@ "when": 1786031983588, "tag": "0012_sloppy_nightcrawler", "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1786464988654, + "tag": "0013_windy_blue_marvel", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1786465264628, + "tag": "0014_fantastic_skrulls", + "breakpoints": true } ] } diff --git a/src/db/schema.ts b/src/db/schema.ts index 76800d8..74d2196 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -245,6 +245,26 @@ export const versions = pgTable( // count query. typeCounts: jsonb('type_counts').$type>(), totalBytes: bigint('total_bytes', { mode: 'number' }).notNull(), + // Set only by a metadata-only patch version, which by construction has the + // same record set as the version it was cut from: it shares that version's + // `version_records` rows instead of copying them. A copy cost one row per + // record per edit — ~2 GB with indexes to fix a readme typo on a 5.5M-record + // collection, repeated for every edit. + // + // NULL means "this version owns its rows", which is every pushed version and + // everything written before this column existed. Always exactly one hop: it + // is set to the base's own pointer when the base is itself a metadata patch, + // so it always names a version that owns rows and never needs a recursive + // resolve. Read it through `recordsVersionId()` — a query that filters + // `version_records` on a patch version's own id silently matches zero rows. + // + // RESTRICT, not CASCADE: deleting a version whose rows others share would + // empty those versions rather than fail. Nothing deletes a shared version + // today, and this is what keeps that true. + recordsFromVersionId: bigint('records_from_version_id', { mode: 'number' }).references( + (): any => versions.id, + { onDelete: 'restrict' }, + ), status: text('status').notNull().default('ready'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }, @@ -478,6 +498,58 @@ export const negotiateSessionManifest = pgTable( ], ) +// --- Metadata Jobs --- + +/** + * Background metadata-only version bumps. + * + * A metadata PATCH creates a patch version, which means folding both version + * digests over the whole record set and copying every `version_records` row. + * Past a few million records that is minutes of database work, and Cloudflare + * cuts the connection at 100s — so the endpoint was simply unreachable at that + * size. `?async=true` returns a job id and finishes the work in the background, + * the same shape the negotiate commit already uses. + * + * Unlike an async negotiate finalize, the version here is built in a single + * transaction with its final `ready` status, so a process that dies mid-job + * leaves nothing behind but this row: Postgres rolls the version back. The + * sweep therefore only has to fail the job, not clean up a partial version. + */ +export const metadataJobs = pgTable( + 'metadata_jobs', + { + id: uuid('id').defaultRandom().primaryKey(), + collectionId: uuid('collection_id') + .notNull() + .references(() => collections.id, { onDelete: 'cascade' }), + // Nullable to match `versions.pushed_by`: a collection-scoped API key can + // act without a user, and the job outlives the request that made it. + userId: text('user_id').references(() => user.id, { onDelete: 'set null' }), + status: text('status', { enum: ['running', 'completed', 'failed'] }) + .notNull() + .default('running'), + // The version this job is bumping from, and the merged metadata it will + // write — enough to explain a failed job without replaying the request. + baseSemver: text('base_semver').notNull(), + metadata: jsonb('metadata').notNull(), + // Outcome, so a client polling after the fact gets the same answer the + // synchronous path would have returned inline. + result: jsonb('result').$type<{ + semver: string + hash: string + metadata: Record + }>(), + error: jsonb('error').$type<{ statusCode: number; error: string; [k: string]: unknown }>(), + startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(), + finishedAt: timestamp('finished_at', { withTimezone: true }), + }, + (t) => [ + // Drives the "is one already running for this collection?" guard, which is + // what keeps two jobs from racing to claim the same semver. + index('metadata_jobs_collection_status_idx').on(t.collectionId, t.status), + ], +) + // --- Sync Runs (mirror mode) --- export const syncRuns = pgTable('sync_runs', { diff --git a/src/lib/version-helpers.server.ts b/src/lib/version-helpers.server.ts index cb7387f..ccde1dc 100644 --- a/src/lib/version-helpers.server.ts +++ b/src/lib/version-helpers.server.ts @@ -129,3 +129,54 @@ export async function getOrgRole( .limit(1) return membership?.role ?? null } + +/** + * Flatten an error and its `cause` chain into one string. + * + * Drizzle puts the SQL in `message` and the underlying Postgres error — the part + * that says *why* — in `cause`. Recording only `message` yields "Failed query: + * SELECT …" with no reason, which is unactionable for whoever is reading a + * failed session or job hours later. + */ +export function describeError(err: unknown): string { + const parts: string[] = [] + let current: unknown = err + for (let depth = 0; current && depth < 5; depth++) { + if (current instanceof Error) { + parts.push(current.message) + // Postgres errors carry the useful specifics outside `message`. + const pg = current as { code?: string; detail?: string; hint?: string } + const extra = [ + pg.code && `code ${pg.code}`, + pg.detail && `detail: ${pg.detail}`, + pg.hint && `hint: ${pg.hint}`, + ].filter(Boolean) + if (extra.length > 0) parts.push(extra.join(', ')) + current = current.cause + } else { + parts.push(String(current)) + break + } + } + return parts.join(' | ') +} + +/** + * The version whose `version_records` rows hold this version's record set. + * + * A metadata-only patch version shares its base's rows rather than copying them, + * so its own id matches nothing in `version_records`. Every query that filters or + * joins that table on `version_id` must go through here. Passing a raw + * `version.id` instead returns an empty record set — no error, just nothing. + * + * The pointer is always one hop by construction (see `versions.records_from_version_id`), + * so this needs no recursion. + * + * `recordsFromVersionId` is deliberately required rather than optional: a version + * fetched with a narrow projection that omitted the column would otherwise + * resolve to its own id and silently read an empty record set. Making it required + * turns that into a compile error at every call site. + */ +export function recordsVersionId(v: { id: number; recordsFromVersionId: number | null }): number { + return v.recordsFromVersionId ?? v.id +} diff --git a/src/routes/[owner]/[collection]/settings.tsx b/src/routes/[owner]/[collection]/settings.tsx index db3600d..fa02c8b 100644 --- a/src/routes/[owner]/[collection]/settings.tsx +++ b/src/routes/[owner]/[collection]/settings.tsx @@ -15,6 +15,50 @@ import { import WebhooksSettings from '~/components/WebhooksSettings' import { useAppContext } from '~/lib/app-context' +/** + * Poll an async metadata job to completion. + * + * Metadata edits create a patch version, whose cost scales with the record set, + * so this legitimately runs for minutes on a large collection. There is no + * timeout: the server-side sweep is what ends a job that will never finish, and + * giving up here would only lose track of a write that is still going to land. + * `onProgress` gets a note once the wait stops looking instant. + */ +async function pollMetadataJob( + owner: string | undefined, + collection: string | undefined, + jobId: string, + onProgress: (message: string) => void, +): Promise<{ semver?: string; error?: string }> { + const INTERVAL_MS = 1500 + const NOTE_AFTER_MS = 4000 + const startedAt = Date.now() + let noted = false + + for (;;) { + await new Promise((resolve) => setTimeout(resolve, INTERVAL_MS)) + + const res = await fetch(`/api/collections/${owner}/${collection}/metadata/jobs/${jobId}`, { + credentials: 'include', + }) + if (!res.ok) { + const body = await res.json().catch(() => ({})) + return { error: body.error ?? 'Lost track of the metadata update.' } + } + + const job = await res.json() + if (job.status === 'completed') return { semver: job.result?.semver } + if (job.status === 'failed') { + return { error: job.error?.error ?? 'Metadata update failed.' } + } + + if (!noted && Date.now() - startedAt > NOTE_AFTER_MS) { + noted = true + onProgress('Saving — this collection is large enough that the new version takes a while.') + } + } +} + export default function CollectionSettingsPage() { const { owner, collection } = useParams() const { currentUser } = useAppContext() @@ -108,27 +152,40 @@ export default function CollectionSettingsPage() { else payload.license = null payload.tags = tags.length > 0 ? tags : null - const res = await fetch(`/api/collections/${owner}/${collection}/metadata`, { + // Always async. Saving metadata creates a patch version, which on a + // multi-million-record collection takes longer than the proxy will hold a + // connection open — a synchronous save there dies at a bare 524 with no way + // to tell whether it landed. Polling costs one extra round trip on small + // collections and is the only thing that works on large ones. + const res = await fetch(`/api/collections/${owner}/${collection}/metadata?async=true`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload), }) - if (res.ok) { - const body = await res.json() - if (body.unchanged) { - setSuccess('No changes to save.') - } else { - setSuccess(`Metadata updated (${body.semver}).`) - } - const refreshed = await fetch(`/api/collections/${owner}/${collection}`, { - credentials: 'include', - }) - if (refreshed.ok) setData(await refreshed.json()) - } else { + + if (!res.ok) { const body = await res.json().catch(() => ({})) setError(body.error ?? 'Metadata update failed.') + return } + + const accepted = await res.json() + if (accepted.unchanged) { + setSuccess('No changes to save.') + return + } + + const outcome = await pollMetadataJob(owner, collection, accepted.job_id, setSuccess) + if (outcome.error) { + setError(outcome.error) + return + } + setSuccess(`Metadata updated (${outcome.semver}).`) + const refreshed = await fetch(`/api/collections/${owner}/${collection}`, { + credentials: 'include', + }) + if (refreshed.ok) setData(await refreshed.json()) } finally { setSubmitting('') } diff --git a/src/routes/docs/api/collections.tsx b/src/routes/docs/api/collections.tsx index 181edf4..35cd121 100644 --- a/src/routes/docs/api/collections.tsx +++ b/src/routes/docs/api/collections.tsx @@ -82,6 +82,13 @@ const metadataRes = `{ } }` +const metadataAsync = `PATCH /api/collections/:owner/:slug/metadata?async=true +→ 202 { "job_id": "3f9c…", "status": "running", "base_semver": "v3.2.0" } + +GET /api/collections/:owner/:slug/metadata/jobs/3f9c… +→ 200 { "job_id": "3f9c…", "status": "completed", + "result": { "semver": "v3.2.1", "hash": "private:e5f6a7b8…", "metadata": { … } } }` + const forkReq = `{ "targetOrg": "my-org", "slug": "my-fork" @@ -238,8 +245,8 @@ export default function DocsApiCollections() {

PATCH /api/collections/:owner/:slug/metadata

Auth: write scope

- Update version metadata by creating a new minor version bump. The request body is a JSON - object whose fields are merged with the previous version's metadata. Use this to update{' '} + Update version metadata by creating a new patch version. The request body is a JSON object + whose fields are merged with the previous version's metadata. Use this to update{' '} description, readme, license, or any other metadata fields without pushing new records.

@@ -287,9 +294,34 @@ export default function DocsApiCollections() {
           {metadataRes}
         
+

Large collections

+

+ A metadata edit creates a patch version, which means recomputing both version digests over + the record set and copying every record-membership row. Past a few million records that + takes longer than a proxy will hold the connection open. Add ?async=true to + get an immediate 202 with a job_id, then poll for the outcome: +

+
+          {metadataAsync}
+        
+

+ The job's status is running, completed or{' '} + failed. On completed, result holds exactly what the + synchronous call would have returned; on failed, error holds the + rejection. Only one metadata update runs per collection at a time. +

Errors

+ + + +
+ 409 + + A metadata update is already in progress for this collection. The response carries + its job_id. +
422 diff --git a/src/routes/docs/integration.tsx b/src/routes/docs/integration.tsx index bf5403f..ea64011 100644 --- a/src/routes/docs/integration.tsx +++ b/src/routes/docs/integration.tsx @@ -231,7 +231,9 @@ export default function DocsIntegration() {

To update metadata without changing records or schemas (e.g. editing the readme),{' '} PATCH /api/collections/:owner/:slug/metadata with the fields to change. This - creates a patch version automatically. + creates a patch version automatically. On a collection of more than a few million records, + add ?async=true and poll the returned job — building the patch version takes + longer than a synchronous request survives.

First Push Example

diff --git a/tools/cleanupSessions.ts b/tools/cleanupSessions.ts index 0c1919e..f50dffd 100644 --- a/tools/cleanupSessions.ts +++ b/tools/cleanupSessions.ts @@ -13,8 +13,11 @@ * half-built version is invisible to readers but still holds version_records * rows. Anything still 'committing' well past the point a finalize could * plausibly still be running is treated as dead. + * + * Does the same for async metadata jobs, which strand the same way and, while + * stranded, block further metadata edits on their collection. */ -import { and, eq, lt, sql } from 'drizzle-orm' +import { and, eq, lt, ne, sql } from 'drizzle-orm' import { db, schema } from '../src/db/client.server.js' @@ -72,8 +75,45 @@ async function sweepStrandedFinalizes() { } } +/** + * Fail out stranded async metadata jobs. + * + * Unlike a negotiate finalize there is no partial version to clean up: the + * metadata path builds its version in a single transaction with its final + * `ready` status, so a process that dies mid-job leaves the version rolled back + * and only the job row behind. Flipping it to 'failed' is what releases the + * per-collection in-progress guard, so without this a dead job would block every + * later metadata edit on that collection. + */ +async function sweepStrandedMetadataJobs() { + const cutoff = new Date(Date.now() - FINALIZE_TIMEOUT_MS) + const stranded = await db + .update(schema.metadataJobs) + .set({ + status: 'failed', + error: { + statusCode: 500, + error: 'Metadata update did not complete — the process handling it went away.', + }, + finishedAt: new Date(), + }) + .where( + and(eq(schema.metadataJobs.status, 'running'), lt(schema.metadataJobs.startedAt, cutoff)), + ) + .returning({ id: schema.metadataJobs.id }) + + if (stranded.length > 0) { + console.log( + `[cleanup-sessions] Failed ${stranded.length} stranded metadata job(s): ${stranded + .map((j) => j.id) + .join(', ')}`, + ) + } +} + async function main() { await sweepStrandedFinalizes() + await sweepStrandedMetadataJobs() const cutoff = new Date(Date.now() - GRACE_MS) const deleted = await db @@ -85,6 +125,19 @@ async function main() { `[cleanup-sessions] Deleted ${deleted.length} negotiate session(s) expired before ${cutoff.toISOString()}`, ) + // Finished metadata jobs are only kept so a client that polls late, or someone + // reading back a failure, still gets an answer. One row per metadata edit, so + // this is housekeeping rather than a real growth problem. + const deletedJobs = await db + .delete(schema.metadataJobs) + .where( + and(ne(schema.metadataJobs.status, 'running'), lt(schema.metadataJobs.finishedAt, cutoff)), + ) + .returning({ id: schema.metadataJobs.id }) + if (deletedJobs.length > 0) { + console.log(`[cleanup-sessions] Deleted ${deletedJobs.length} finished metadata job(s)`) + } + // Manifest rows cascade with their session; report the remaining footprint const [counts] = await db .select({ diff --git a/tools/verifyRecordSharing.ts b/tools/verifyRecordSharing.ts new file mode 100644 index 0000000..30b037f --- /dev/null +++ b/tools/verifyRecordSharing.ts @@ -0,0 +1,240 @@ +/** + * Verify that a metadata-only patch version sharing its base's `version_records` + * rows is indistinguishable from one that copied them. + * + * Not part of `pnpm test`: it needs a real Postgres, and the rest of the suite is + * pure unit tests that run anywhere. Point it at a SCRATCH database — it writes a + * fixture and does not clean up: + * + * createdb underlay_vrshare_test + * DATABASE_URL=postgresql://localhost:5432/underlay_vrshare_test pnpm db:migrate + * DATABASE_URL=postgresql://localhost:5432/underlay_vrshare_test pnpm tool:verifyRecordSharing + * + * The one that matters is "UNRESOLVED listing returns 0": that is the silent + * failure every call site resolving through `recordsVersionId()` exists to avoid. + */ +import { eq, or, sql } from 'drizzle-orm' + +import { db, schema } from '../src/db/client.server.js' +import { hashRecord } from '../src/lib/core/hash.js' +import { recordsVersionId } from '../src/lib/version-helpers.server.js' + +const fail: string[] = [] +const check = (name: string, cond: boolean, detail = '') => { + if (cond) console.log(` ok ${name}`) + else { + console.log(` FAIL ${name} ${detail}`) + fail.push(name) + } +} + +async function main() { + // --- fixture --- + const orgId = 'org_test' + await db + .insert(schema.organization) + .values({ id: orgId, name: 'Test Org', slug: 'test-org', createdAt: new Date() }) + .onConflictDoNothing() + + const [coll] = await db + .insert(schema.collections) + .values({ organizationId: orgId, slug: 'c1', name: 'C1', public: true }) + .returning() + + const RECORDS = [ + { id: 'r1', type: 'Thing', data: { a: 1 } }, + { id: 'r2', type: 'Thing', data: { a: 2 } }, + { id: 'r3', type: 'Other', data: { a: 3 } }, + ] + const withHash = RECORDS.map((r) => ({ ...r, hash: hashRecord(r).hash })) + await db + .insert(schema.recordObjects) + .values( + withHash.map((r) => ({ + hash: r.hash, + recordId: r.id, + type: r.type, + data: r.data, + size: JSON.stringify(r.data).length, + })), + ) + .onConflictDoNothing() + + // base version owns its rows + const [base] = await db + .insert(schema.versions) + .values({ + collectionId: coll!.id, + semver: 'v1.0.0', + major: 1, + minor: 0, + patch: 0, + hash: 'private:base', + publicHash: 'public:base', + recordCount: withHash.length, + fileCount: 0, + totalBytes: 10, + metadata: { readme: 'old' }, + typeCounts: { Thing: 2, Other: 1 }, + }) + .returning() + + await db.insert(schema.versionRecords).values( + withHash.map((r) => ({ + versionId: base!.id, + recordHash: r.hash, + recordId: r.id, + type: r.type, + private: false, + })), + ) + + // metadata patch shares the base's rows + const [patch] = await db + .insert(schema.versions) + .values({ + collectionId: coll!.id, + semver: 'v1.0.1', + major: 1, + minor: 0, + patch: 1, + hash: 'private:patch', + publicHash: 'public:patch', + baseSemver: 'v1.0.0', + recordCount: base!.recordCount, + fileCount: 0, + totalBytes: base!.totalBytes, + metadata: { readme: 'new' }, + typeCounts: base!.typeCounts, + recordsFromVersionId: recordsVersionId(base!), + }) + .returning() + + console.log(`\nbase=${base!.id} patch=${patch!.id} sharesFrom=${patch!.recordsFromVersionId}\n`) + + // --- assertions --- + check('patch owns zero rows of its own', await countRows(patch!.id, false), '') + check('pointer names the base', patch!.recordsFromVersionId === base!.id) + check('recordsVersionId(patch) === base.id', recordsVersionId(patch!) === base!.id) + + // record listing, the way versions.ts does it + const listed = await db + .select({ recordId: schema.versionRecords.recordId }) + .from(schema.versionRecords) + .where(eq(schema.versionRecords.versionId, recordsVersionId(patch!))) + check('resolved listing returns all 3 records', listed.length === 3, `got ${listed.length}`) + + const unresolved = await db + .select({ recordId: schema.versionRecords.recordId }) + .from(schema.versionRecords) + .where(eq(schema.versionRecords.versionId, patch!.id)) + check( + 'UNRESOLVED listing returns 0 — this is the failure mode', + unresolved.length === 0, + `got ${unresolved.length}`, + ) + + // raw-SQL streaming path (digest fold source) + const client = db.$client + const streamed: string[] = [] + await client` + SELECT record_hash AS h FROM version_records + WHERE version_id = ${recordsVersionId(patch!)} + ORDER BY record_hash COLLATE "C" + `.cursor(1000, (rows) => { + for (const row of rows) streamed.push(row['h'] as string) + }) + check('digest stream sees all 3 hashes', streamed.length === 3, `got ${streamed.length}`) + check('stream is byte-sorted', streamed.join() === [...streamed].sort().join(), streamed.join()) + + // diff base→patch must be empty in both directions + const diff = (selfId: number, otherId: number) => sql` + SELECT count(*)::int AS n FROM version_records vr + WHERE vr.version_id = ${selfId} + AND NOT EXISTS ( + SELECT 1 FROM version_records s + WHERE s.version_id = ${otherId} AND s.record_id = vr.record_id + )` + const [added] = (await db.execute( + diff(recordsVersionId(patch!), recordsVersionId(base!)), + )) as unknown as { n: number }[] + const [removed] = (await db.execute( + diff(recordsVersionId(base!), recordsVersionId(patch!)), + )) as unknown as { n: number }[] + check('diff added === 0', added!.n === 0, `got ${added!.n}`) + check('diff removed === 0', removed!.n === 0, `got ${removed!.n}`) + + // provenance: the OR join must find BOTH versions for a shared record + const refs = await db + .select({ semver: schema.versions.semver }) + .from(schema.versionRecords) + .innerJoin( + schema.versions, + or( + eq(schema.versionRecords.versionId, schema.versions.id), + eq(schema.versionRecords.versionId, schema.versions.recordsFromVersionId), + ), + ) + .where(eq(schema.versionRecords.recordHash, withHash[0]!.hash)) + const semvers = refs.map((r) => r.semver).sort() + check( + 'provenance lists base AND patch', + semvers.join() === 'v1.0.0,v1.0.1', + semvers.join() || '(none)', + ) + + // RESTRICT must refuse to delete a shared base + let restricted = false + try { + await db.delete(schema.versions).where(eq(schema.versions.id, base!.id)) + } catch { + restricted = true + } + check('deleting a shared base is refused by RESTRICT', restricted) + + // a second consecutive metadata edit must stay one hop + const [patch2] = await db + .insert(schema.versions) + .values({ + collectionId: coll!.id, + semver: 'v1.0.2', + major: 1, + minor: 0, + patch: 2, + hash: 'private:patch2', + publicHash: 'public:patch2', + recordCount: base!.recordCount, + fileCount: 0, + totalBytes: base!.totalBytes, + metadata: { readme: 'newer' }, + recordsFromVersionId: recordsVersionId(patch!), + }) + .returning() + check( + 'chained patch points at the OWNER, not the patch', + patch2!.recordsFromVersionId === base!.id, + `points at ${patch2!.recordsFromVersionId}, base is ${base!.id}`, + ) + const listed2 = await db + .select({ recordId: schema.versionRecords.recordId }) + .from(schema.versionRecords) + .where(eq(schema.versionRecords.versionId, recordsVersionId(patch2!))) + check('chained patch resolves to 3 records', listed2.length === 3, `got ${listed2.length}`) + + console.log( + fail.length === 0 ? '\nALL PASSED\n' : `\n${fail.length} FAILED: ${fail.join(', ')}\n`, + ) + process.exit(fail.length === 0 ? 0 : 1) +} + +async function countRows(versionId: number, expectSome: boolean) { + const [row] = (await db.execute( + sql`SELECT count(*)::int AS n FROM version_records WHERE version_id = ${versionId}`, + )) as unknown as { n: number }[] + return expectSome ? row!.n > 0 : row!.n === 0 +} + +main().catch((err) => { + console.error(err) + process.exit(1) +})