@tangen/db 0.0.0-stage → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +73 -2
  2. package/drizzle/0000_flimsy_shotgun.sql +339 -0
  3. package/drizzle/0001_app_data.sql +11 -0
  4. package/drizzle/0002_add_indexes.sql +36 -0
  5. package/drizzle/0003_extension_data_rename.sql +3 -0
  6. package/drizzle/0004_extension_data_published_globals.sql +12 -0
  7. package/drizzle/0005_auth.sql +51 -0
  8. package/drizzle/0006_auth_identities.sql +13 -0
  9. package/drizzle/0007_ai.sql +18 -0
  10. package/drizzle/0008_ai_chat_threads.sql +5 -0
  11. package/drizzle/0009_forms_source_slug.sql +11 -0
  12. package/drizzle/0010_rbac_roles_status.sql +14 -0
  13. package/drizzle/0011_jobs_v2.sql +67 -0
  14. package/drizzle/0012_search_platform.sql +36 -0
  15. package/drizzle/0013_search_synonym_label.sql +2 -0
  16. package/drizzle/0014_object_types_disabled.sql +2 -0
  17. package/drizzle/0015_playground_collections.sql +25 -0
  18. package/drizzle/0016_catchup_activity_promotion.sql +36 -0
  19. package/drizzle/0017_fields_disabled.sql +3 -0
  20. package/drizzle/0018_object_types_plural.sql +1 -0
  21. package/drizzle/0019_member_profile.sql +2 -0
  22. package/drizzle/0020_paused_jobs.sql +7 -0
  23. package/drizzle/0021_form_submission_notes.sql +2 -0
  24. package/drizzle/0022_paused_jobs_env_reason.sql +4 -0
  25. package/drizzle/0023_scale_indexes.sql +3 -0
  26. package/drizzle/0024_migration_runs.sql +24 -0
  27. package/drizzle/0025_porter_transfer.sql +30 -0
  28. package/drizzle/0026_cron_timezone.sql +1 -0
  29. package/drizzle/0027_rate_limit_counters.sql +6 -0
  30. package/drizzle/0028_realtime_events.sql +10 -0
  31. package/drizzle/0030_delivery_artifacts.sql +10 -0
  32. package/drizzle/0031_realtime_presence.sql +8 -0
  33. package/drizzle/0032_schema_pushes.sql +15 -0
  34. package/drizzle/0033_studio_design_capability.sql +37 -0
  35. package/drizzle/0034_object_types_provided_by.sql +12 -0
  36. package/drizzle/0035_custom_table_owners.sql +24 -0
  37. package/drizzle/0036_revision_version_unique.sql +42 -0
  38. package/drizzle/0037_job_queue_dedupe.sql +20 -0
  39. package/drizzle/0038_search_documents.sql +87 -0
  40. package/drizzle/0039_job_prune_indexes.sql +25 -0
  41. package/drizzle/0040_extension_data_object_idx.sql +10 -0
  42. package/drizzle/0041_knowledge.sql +79 -0
  43. package/drizzle/0042_schema_pushes_checksum.sql +7 -0
  44. package/drizzle/0043_guard_adoption.sql +12 -0
  45. package/drizzle/0044_retention_indexes.sql +7 -0
  46. package/drizzle/0045_extension_install_per_env.sql +40 -0
  47. package/drizzle/0046_extension_migrations.sql +18 -0
  48. package/drizzle/0047_environment_role.sql +25 -0
  49. package/drizzle/0048_environment_role_adoption.sql +30 -0
  50. package/drizzle/0049_release_lifecycle.sql +55 -0
  51. package/drizzle/0050_member_env_membership.sql +21 -0
  52. package/drizzle/0051_remove_environments.sql +350 -0
  53. package/drizzle/0052_release_actor_columns.sql +9 -0
  54. package/drizzle/0053_kernel_event_record.sql +26 -0
  55. package/drizzle/0054_kernel_run_identity.sql +13 -0
  56. package/drizzle/0055_kernel_identities.sql +19 -0
  57. package/drizzle/0056_forms_leave_core.sql +7 -0
  58. package/drizzle/0057_auth_leave_core.sql +8 -0
  59. package/drizzle/0058_member_grants.sql +11 -0
  60. package/drizzle/0059_extension_granted_grants.sql +7 -0
  61. package/drizzle/0060_connections.sql +21 -0
  62. package/drizzle/meta/0000_snapshot.json +1692 -0
  63. package/drizzle/meta/0002_snapshot.json +1935 -0
  64. package/drizzle/meta/0003_snapshot.json +1935 -0
  65. package/drizzle/meta/_journal.json +426 -0
  66. package/package.json +85 -4
  67. package/src/cache-keys.ts +43 -0
  68. package/src/client.ts +136 -0
  69. package/src/content-ddl.ts +70 -0
  70. package/src/content-tables.ts +110 -0
  71. package/src/custom-tables-loader.ts +65 -0
  72. package/src/ensure-system-schema.ts +195 -0
  73. package/src/extension-install-state.ts +220 -0
  74. package/src/extension-migrations.ts +270 -0
  75. package/src/generated/content-tables.ts +271 -0
  76. package/src/generated/custom-tables.ts +18 -0
  77. package/src/index.ts +24 -0
  78. package/src/instrument.ts +139 -0
  79. package/src/knowledge-schema.ts +232 -0
  80. package/src/migrate.ts +67 -0
  81. package/src/prepare-policy.ts +118 -0
  82. package/src/reconcile.ts +115 -0
  83. package/src/schema/ai-chats.ts +36 -0
  84. package/src/schema/ai-settings.ts +24 -0
  85. package/src/schema/api-keys.ts +37 -0
  86. package/src/schema/audit-log.ts +54 -0
  87. package/src/schema/connections.ts +35 -0
  88. package/src/schema/custom-table-owners.ts +32 -0
  89. package/src/schema/delivery-artifacts.ts +33 -0
  90. package/src/schema/entity-tags.ts +33 -0
  91. package/src/schema/extension-data.ts +38 -0
  92. package/src/schema/extension-migrations.ts +42 -0
  93. package/src/schema/fields.ts +37 -0
  94. package/src/schema/globals.ts +35 -0
  95. package/src/schema/identities.ts +27 -0
  96. package/src/schema/index.ts +51 -0
  97. package/src/schema/jobs.ts +104 -0
  98. package/src/schema/knowledge.ts +85 -0
  99. package/src/schema/media.ts +52 -0
  100. package/src/schema/member-grants.ts +34 -0
  101. package/src/schema/members.ts +49 -0
  102. package/src/schema/migration-runs.ts +46 -0
  103. package/src/schema/object-types.ts +41 -0
  104. package/src/schema/objects.ts +38 -0
  105. package/src/schema/paused-jobs.ts +25 -0
  106. package/src/schema/playground.ts +43 -0
  107. package/src/schema/porter-transfer.ts +47 -0
  108. package/src/schema/realtime.ts +45 -0
  109. package/src/schema/releases.ts +95 -0
  110. package/src/schema/roles.ts +27 -0
  111. package/src/schema/saved-views.ts +33 -0
  112. package/src/schema/schema-pushes.ts +43 -0
  113. package/src/schema/search-documents.ts +47 -0
  114. package/src/schema/search-query-log.ts +24 -0
  115. package/src/schema/search-recommendations.ts +27 -0
  116. package/src/schema/search-synonyms.ts +23 -0
  117. package/src/schema/webhook-deliveries.ts +37 -0
  118. package/src/schema/webhooks.ts +36 -0
  119. package/src/schema/workspace-activity.ts +35 -0
  120. package/src/schema/workspace-domains.ts +24 -0
  121. package/src/schema/workspace-installed-apps.ts +48 -0
  122. package/src/schema/workspaces.ts +24 -0
  123. package/src/schema-kit.ts +64 -0
  124. package/src/sealed.ts +84 -0
  125. package/src/seed/skeleton.ts +77 -0
  126. package/src/sql/apply-gin-indexes.ts +31 -0
  127. package/src/sql/gin-indexes.sql +11 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * File Name: extension-migrations.ts
3
+ * Purpose: The extension migration runner (closure plan Phase 4, Unit 5).
4
+ * An extension declares SQL migrations in order; INSTALL and
5
+ * UPGRADE run the pending ones for one workspace x environment,
6
+ * each in its own transaction with its ledger row, and stop at
7
+ * the first failure. History is never rewritten: a changed
8
+ * checksum, a migration the extension no longer declares, or one
9
+ * inserted before applied history refuses the whole run.
10
+ *
11
+ * Author: Raghunath J
12
+ * License: MIT
13
+ *
14
+ * Dependencies/Notes: Lives in @tangen/db so the API's admin install and
15
+ * the CLI run the same code over the same ledger. Plain SQL over a
16
+ * `MigrationDb` (an executor plus a transaction) with adapters for
17
+ * postgres.js and PGlite. Migrations are SQL text, so their
18
+ * checksum is the truth of what ran; they run once per workspace x
19
+ * environment the extension is installed in, and physical
20
+ * structures are shared across scopes, so an extension writes
21
+ * them idempotently (`CREATE TABLE IF NOT EXISTS ...`). Nothing
22
+ * here is called at import, REGISTER, ENABLE or START.
23
+ */
24
+ import { createHash } from 'node:crypto'
25
+
26
+ /** One declared migration: a stable id and the SQL it runs. (Mirrors ExtensionMigration in @tangen/types.) */
27
+ export interface DeclaredMigration {
28
+ id: string
29
+ sql: string
30
+ }
31
+
32
+ export type MigrationExec = (text: string, params?: unknown[]) => Promise<Array<Record<string, unknown>>>
33
+
34
+ export interface MigrationDb {
35
+ exec: MigrationExec
36
+ /** Runs `fn` in one transaction; a throw rolls it back and rethrows. */
37
+ transaction<T>(fn: (exec: MigrationExec) => Promise<T>): Promise<T>
38
+ }
39
+
40
+ /** postgres.js: `sql.begin` reserves one connection for the transaction. */
41
+ export function migrationDbFromPostgres(sql: {
42
+ unsafe: (text: string, params?: unknown[]) => Promise<unknown>
43
+ begin: <T>(fn: (tx: { unsafe: (text: string, params?: unknown[]) => Promise<unknown> }) => Promise<T>) => Promise<T>
44
+ }): MigrationDb {
45
+ const wrap = (u: (text: string, params?: unknown[]) => Promise<unknown>): MigrationExec =>
46
+ async (text, params) => (await u(text, params && params.length > 0 ? params : undefined)) as Array<Record<string, unknown>>
47
+ return { exec: wrap(sql.unsafe.bind(sql)), transaction: (fn) => sql.begin((tx) => fn(wrap(tx.unsafe.bind(tx)))) }
48
+ }
49
+
50
+ /** PGlite (tests): `query` for parameterised statements, `exec` for multi-statement SQL text. */
51
+ export function migrationDbFromPglite(client: {
52
+ query: (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
53
+ exec: (text: string) => Promise<unknown>
54
+ transaction: <T>(fn: (tx: { query: (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>; exec: (text: string) => Promise<unknown> }) => Promise<T>) => Promise<T>
55
+ }): MigrationDb {
56
+ const wrap = (c: { query: (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>; exec: (text: string) => Promise<unknown> }): MigrationExec =>
57
+ async (text, params) => {
58
+ if (!params || params.length === 0) { await c.exec(text); return [] }
59
+ return (await c.query(text, params)).rows as Array<Record<string, unknown>>
60
+ }
61
+ return {
62
+ exec: async (text, params) => (await client.query(text, params ?? [])).rows as Array<Record<string, unknown>>,
63
+ transaction: (fn) => client.transaction((tx) => fn(wrap(tx))),
64
+ }
65
+ }
66
+
67
+ /** The MigrationDb behind a drizzle client, whichever driver it wraps. */
68
+ export function migrationDbFromDrizzle(db: unknown): MigrationDb {
69
+ const d = db as { $rawClient?: unknown; session?: { client?: unknown }; $client?: unknown }
70
+ const raw = (d.$rawClient ?? d.session?.client) as Parameters<typeof migrationDbFromPostgres>[0] | undefined
71
+ if (raw && typeof raw.begin === 'function') return migrationDbFromPostgres(raw)
72
+ const pg = d.$client as Parameters<typeof migrationDbFromPglite>[0] | undefined
73
+ if (pg && typeof pg.transaction === 'function') return migrationDbFromPglite(pg)
74
+ throw new Error('migrationDbFromDrizzle: not a drizzle client this platform knows')
75
+ }
76
+
77
+ const MIGRATION_CHECKSUM_VERSION = 1
78
+ export const MIGRATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/
79
+
80
+ /** sha256 over the migration id and its SQL; the ledger's truth of what ran. */
81
+ export function migrationChecksum(m: DeclaredMigration): string {
82
+ return createHash('sha256').update(JSON.stringify({ v: MIGRATION_CHECKSUM_VERSION, id: m.id, sql: m.sql })).digest('hex')
83
+ }
84
+
85
+ export interface MigrationScope { workspaceId: string; extensionId: string }
86
+
87
+ export interface LedgerRow {
88
+ migrationId: string
89
+ checksum: string
90
+ status: string
91
+ extensionVersion: string | null
92
+ error: string | null
93
+ appliedAt: Date | string
94
+ }
95
+
96
+ export interface MigrationPlan {
97
+ /** Declared and not yet applied (or previously failed), in declared order. */
98
+ pending: DeclaredMigration[]
99
+ applied: string[]
100
+ /** Previously failed rows the run will retry. */
101
+ retrying: string[]
102
+ /** Why the run must not proceed; empty when it may. */
103
+ refusals: string[]
104
+ }
105
+
106
+ /** Validates the declaration itself: ids, uniqueness, SQL present. */
107
+ export function validateDeclaredMigrations(migrations: unknown): string[] {
108
+ const errors: string[] = []
109
+ if (!Array.isArray(migrations)) return ['migrations must be an array of { id, sql }']
110
+ const seen = new Set<string>()
111
+ migrations.forEach((m, i) => {
112
+ const id = (m as { id?: unknown })?.id
113
+ const sql = (m as { sql?: unknown })?.sql
114
+ if (typeof id !== 'string' || !MIGRATION_ID_PATTERN.test(id)) { errors.push(`migrations[${i}].id must match ${MIGRATION_ID_PATTERN}`); return }
115
+ if (seen.has(id)) errors.push(`migrations[${i}]: '${id}' is declared twice`)
116
+ seen.add(id)
117
+ if (typeof sql !== 'string' || sql.trim() === '') errors.push(`migrations[${i}] '${id}': sql must be a non-empty string`)
118
+ })
119
+ return errors
120
+ }
121
+
122
+ /**
123
+ * Pure: what a run would do, from the declared list and the ledger rows of
124
+ * the scope. Refuses when history and declaration disagree.
125
+ */
126
+ export function planExtensionMigrations(declared: DeclaredMigration[], ledger: LedgerRow[]): MigrationPlan {
127
+ const refusals = validateDeclaredMigrations(declared)
128
+ const byId = new Map(ledger.map((r) => [r.migrationId, r]))
129
+ const declaredIds = new Set(declared.map((m) => m.id))
130
+ for (const r of ledger) {
131
+ if (r.status === 'applied' && !declaredIds.has(r.migrationId)) refusals.push(`the ledger has '${r.migrationId}' applied, which the extension no longer declares`)
132
+ }
133
+ const pending: DeclaredMigration[] = []
134
+ const applied: string[] = []
135
+ const retrying: string[] = []
136
+ let sawPending = false
137
+ for (const m of declared) {
138
+ const row = byId.get(m.id)
139
+ if (row?.status === 'applied') {
140
+ if (row.checksum !== migrationChecksum(m)) refusals.push(`'${m.id}' was applied with a different SQL (checksum ${row.checksum.slice(0, 12)}, now ${migrationChecksum(m).slice(0, 12)})`)
141
+ if (sawPending) refusals.push(`'${m.id}' is applied but a migration declared before it is not: the order changed after it ran`)
142
+ applied.push(m.id)
143
+ continue
144
+ }
145
+ if (row?.status === 'failed') retrying.push(m.id)
146
+ sawPending = true
147
+ pending.push(m)
148
+ }
149
+ return { pending, applied, retrying, refusals }
150
+ }
151
+
152
+ export interface RunMigrationsInput extends MigrationScope {
153
+ extensionVersion: string | null
154
+ migrations: DeclaredMigration[]
155
+ /** Report only; run nothing. */
156
+ dryRun?: boolean
157
+ }
158
+
159
+ export interface RunMigrationsResult {
160
+ ok: boolean
161
+ plan: MigrationPlan
162
+ applied: string[]
163
+ /** The migration that failed and why; the ones after it did not run. */
164
+ failed: { id: string; error: string } | null
165
+ /** Migrations recorded for this environment without running their SQL: another environment in this database had applied them. */
166
+ adopted?: string[]
167
+ /** Ready to print. */
168
+ message: string
169
+ }
170
+
171
+ const ROW_COLUMNS = `migration_id AS "migrationId", checksum, status, extension_version AS "extensionVersion", error, applied_at AS "appliedAt"`
172
+
173
+ /** The ledger rows of one extension in one scope, oldest first. */
174
+ export async function readExtensionMigrationLedger(exec: MigrationExec, scope: MigrationScope): Promise<LedgerRow[]> {
175
+ const rows = await exec(
176
+ `SELECT ${ROW_COLUMNS} FROM extension_migrations WHERE workspace_id = $1 AND extension_id = $2 ORDER BY applied_at, migration_id`,
177
+ [scope.workspaceId, scope.extensionId],
178
+ )
179
+ return rows as unknown as LedgerRow[]
180
+ }
181
+
182
+ /** The ledger of every extension in a scope, grouped by extension (the gate's one query). */
183
+ export async function readScopeMigrationLedger(exec: MigrationExec, scope: { workspaceId: string }): Promise<Record<string, LedgerRow[]>> {
184
+ const rows = await exec(
185
+ `SELECT extension_id AS "extensionId", ${ROW_COLUMNS} FROM extension_migrations WHERE workspace_id = $1 ORDER BY applied_at, migration_id`,
186
+ [scope.workspaceId],
187
+ )
188
+ const out: Record<string, LedgerRow[]> = {}
189
+ for (const r of rows as unknown as Array<LedgerRow & { extensionId: string }>) (out[r.extensionId] ??= []).push(r)
190
+ return out
191
+ }
192
+
193
+ /**
194
+ * Runs the pending migrations of one extension in one scope. Each
195
+ * migration runs in its own transaction together with its ledger row, so
196
+ * a failure leaves neither half; the failure is then recorded as a
197
+ * `failed` row (retried next time) and the run stops. The result never
198
+ * reports more than what actually ran.
199
+ */
200
+ export async function runExtensionMigrations(db: MigrationDb, input: RunMigrationsInput): Promise<RunMigrationsResult> {
201
+ const ledger = await readExtensionMigrationLedger(db.exec, input)
202
+ const plan = planExtensionMigrations(input.migrations, ledger)
203
+ const label = `${input.extensionId} in ${input.workspaceId}`
204
+ if (plan.refusals.length > 0) {
205
+ return { ok: false, plan, applied: [], failed: null, message: `Refused: the migration history of ${label} does not match its declaration. Nothing ran.\n${plan.refusals.map((r) => ` - ${r}`).join('\n')}` }
206
+ }
207
+ if (plan.pending.length === 0) return { ok: true, plan, applied: [], failed: null, message: `${label}: schema up to date (${plan.applied.length} applied)` }
208
+ if (input.dryRun) return { ok: true, plan, applied: [], failed: null, message: `${label}: ${plan.pending.length} pending: ${plan.pending.map((m) => m.id).join(', ')}` }
209
+
210
+ const applied: string[] = []
211
+ const adopted: string[] = []
212
+ for (const m of plan.pending) {
213
+ const checksum = migrationChecksum(m)
214
+ const rowId = `xmig_${createHash('sha256').update(`${input.workspaceId}:${input.extensionId}:${m.id}`).digest('hex').slice(0, 24)}`
215
+ // Physical schema is DATABASE-scoped; the ledger is per environment
216
+ // (plan section 16). When another environment (or workspace) in this
217
+ // database already applied this exact migration, its SQL has run against
218
+ // the tables this environment will use: record this environment's row
219
+ // and run nothing, whether or not the SQL was written idempotently.
220
+ const present = await db.exec(
221
+ `SELECT 1 FROM extension_migrations WHERE extension_id = $1 AND migration_id = $2 AND checksum = $3 AND status = 'applied' LIMIT 1`,
222
+ [input.extensionId, m.id, checksum],
223
+ )
224
+ const physicallyApplied = present.length > 0
225
+ try {
226
+ await db.transaction(async (tx) => {
227
+ if (!physicallyApplied) await tx(m.sql)
228
+ await tx(
229
+ `INSERT INTO extension_migrations (id, workspace_id, extension_id, migration_id, extension_version, checksum, status, error, applied_at)
230
+ VALUES ($1, $2, $3, $4, $5, $6, 'applied', NULL, now())
231
+ ON CONFLICT (workspace_id, extension_id, migration_id)
232
+ DO UPDATE SET checksum = EXCLUDED.checksum, extension_version = EXCLUDED.extension_version, status = 'applied', error = NULL, applied_at = now()`,
233
+ [rowId, input.workspaceId, input.extensionId, m.id, input.extensionVersion, checksum],
234
+ )
235
+ })
236
+ ;(physicallyApplied ? adopted : applied).push(m.id)
237
+ } catch (e) {
238
+ const error = e instanceof Error ? e.message : String(e)
239
+ // Outside the rolled-back transaction: the diagnosable trace of what failed.
240
+ await db.exec(
241
+ `INSERT INTO extension_migrations (id, workspace_id, extension_id, migration_id, extension_version, checksum, status, error, applied_at)
242
+ VALUES ($1, $2, $3, $4, $5, $6, 'failed', $7, now())
243
+ ON CONFLICT (workspace_id, extension_id, migration_id)
244
+ DO UPDATE SET checksum = EXCLUDED.checksum, extension_version = EXCLUDED.extension_version, status = 'failed', error = EXCLUDED.error, applied_at = now()`,
245
+ [rowId, input.workspaceId, input.extensionId, m.id, input.extensionVersion, checksum, error],
246
+ ).catch(() => {})
247
+ const remaining = plan.pending.slice(plan.pending.indexOf(m) + 1).map((x) => x.id)
248
+ return {
249
+ ok: false, plan, applied, failed: { id: m.id, error },
250
+ message: `${label}: migration '${m.id}' failed and was rolled back: ${error}${applied.length > 0 ? `\n applied before it: ${applied.join(', ')}` : ''}${remaining.length > 0 ? `\n not run: ${remaining.join(', ')}` : ''}\n Fix the migration and run the install or upgrade again; the ledger keeps the failure until it succeeds.`,
251
+ }
252
+ }
253
+ }
254
+ const adoptedNote = adopted.length ? `; ${adopted.length} already present in this database from another workspace (${adopted.join(', ')}), recorded without running` : ''
255
+ return { ok: true, plan, applied, adopted, failed: null, message: `${label}: applied ${applied.length} migration${applied.length === 1 ? '' : 's'} (${applied.join(', ')})${adoptedNote}` }
256
+ }
257
+
258
+ /**
259
+ * What the gate and doctor ask: is the declared schema there? Null when it
260
+ * is; otherwise a reason an operator can act on.
261
+ */
262
+ export function explainPendingSchema(declared: DeclaredMigration[], ledger: LedgerRow[]): string | null {
263
+ if (declared.length === 0) return null
264
+ const plan = planExtensionMigrations(declared, ledger)
265
+ if (plan.refusals.length > 0) return `its migration history does not match its declaration: ${plan.refusals[0]}`
266
+ const failed = ledger.find((r) => r.status === 'failed')
267
+ if (failed) return `migration '${failed.migrationId}' failed here: ${failed.error ?? 'unknown error'}; run the install or upgrade again`
268
+ if (plan.pending.length > 0) return `${plan.pending.length} migration${plan.pending.length === 1 ? '' : 's'} not applied here (${plan.pending.map((m) => m.id).join(', ')}); run the install or upgrade`
269
+ return null
270
+ }
@@ -0,0 +1,271 @@
1
+ // DO NOT EDIT - generated by tangen push
2
+ // Re-run `tangen push` to regenerate after schema changes.
3
+
4
+ import { pgTable, text, numeric, boolean, timestamp, jsonb, index, uniqueIndex, sql, primaryKey } from '@tangen/db/schema-kit'
5
+
6
+ function systemCols() {
7
+ return {
8
+ // Identity is COMPOSITE - (id) - declared per table below.
9
+ id: text('id').notNull(),
10
+ workspaceId: text('workspace_id').notNull(),
11
+
12
+ title: text('title'),
13
+ slug: text('slug'),
14
+ status: text('status').notNull().default('draft'),
15
+ publishedAt: timestamp('published_at', { withTimezone: true }),
16
+ publishedBy: text('published_by'),
17
+ publishedValues: jsonb('published_values'),
18
+ scheduledAt: timestamp('scheduled_at', { withTimezone: true }),
19
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
20
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
21
+ createdBy: text('created_by'),
22
+ updatedBy: text('updated_by'),
23
+ }
24
+ }
25
+
26
+ export const author = pgTable('author', {
27
+ ...systemCols(),
28
+ bio: text('bio'),
29
+ avatar: text('avatar'),
30
+ role: text('role'),
31
+ active: boolean('active'),
32
+ }, t => ({
33
+ authorPk: primaryKey({ columns: [t.id] }),
34
+ authorScopeIdx: index('author_scope_idx').on(t.workspaceId, t.status, t.createdAt),
35
+ authorSlugUniq: uniqueIndex('author_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
36
+ }))
37
+
38
+ export const coupon = pgTable('coupon', {
39
+ ...systemCols(),
40
+ code: text('code'),
41
+ discount_type: text('discount_type'),
42
+ value: numeric('value'),
43
+ min_amount: numeric('min_amount'),
44
+ active: boolean('active'),
45
+ archived: boolean('archived'),
46
+ }, t => ({
47
+ couponPk: primaryKey({ columns: [t.id] }),
48
+ couponScopeIdx: index('coupon_scope_idx').on(t.workspaceId, t.status, t.createdAt),
49
+ couponSlugUniq: uniqueIndex('coupon_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
50
+ }))
51
+
52
+ export const page = pgTable('page', {
53
+ ...systemCols(),
54
+ shortDescription: text('shortDescription'),
55
+ pageType: text('pageType'),
56
+ featuredImage: text('featuredImage'),
57
+ metaTitle: text('metaTitle'),
58
+ metaDescription: text('metaDescription'),
59
+ canonicalUrl: text('canonicalUrl'),
60
+ ogImage: text('ogImage'),
61
+ sections: jsonb('sections'),
62
+ random_form: text('random_form'),
63
+ }, t => ({
64
+ pagePk: primaryKey({ columns: [t.id] }),
65
+ pageScopeIdx: index('page_scope_idx').on(t.workspaceId, t.status, t.createdAt),
66
+ pageSlugUniq: uniqueIndex('page_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
67
+ }))
68
+
69
+ export const pincode = pgTable('pincode', {
70
+ ...systemCols(),
71
+ pincode: text('pincode'),
72
+ city: text('city'),
73
+ }, t => ({
74
+ pincodePk: primaryKey({ columns: [t.id] }),
75
+ pincodeScopeIdx: index('pincode_scope_idx').on(t.workspaceId, t.status, t.createdAt),
76
+ pincodeSlugUniq: uniqueIndex('pincode_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
77
+ }))
78
+
79
+ export const pincode_city = pgTable('pincode_city', {
80
+ ...systemCols(),
81
+ region: text('region'),
82
+ position: numeric('position'),
83
+ }, t => ({
84
+ pincode_cityPk: primaryKey({ columns: [t.id] }),
85
+ pincode_cityScopeIdx: index('pincode_city_scope_idx').on(t.workspaceId, t.status, t.createdAt),
86
+ pincode_citySlugUniq: uniqueIndex('pincode_city_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
87
+ }))
88
+
89
+ export const post = pgTable('post', {
90
+ ...systemCols(),
91
+ subtitle: text('subtitle'),
92
+ body: jsonb('body'),
93
+ coverImage: text('coverImage'),
94
+ heroImage: text('heroImage'),
95
+ attachment: text('attachment'),
96
+ categories: text('categories').array(),
97
+ primaryCategory: text('primaryCategory'),
98
+ tags: text('tags').array(),
99
+ readingMinutes: numeric('readingMinutes'),
100
+ featured: boolean('featured'),
101
+ publishDate: timestamp('publishDate', { withTimezone: true }),
102
+ excerpt: text('excerpt'),
103
+ author: text('author'),
104
+ relatedPosts: text('relatedPosts').array(),
105
+ bingo: text('bingo').array(),
106
+ featuredItems: jsonb('featuredItems'),
107
+ primaryItem: jsonb('primaryItem'),
108
+ metadata: jsonb('metadata'),
109
+ faqs: jsonb('faqs'),
110
+ sections: jsonb('sections'),
111
+ }, t => ({
112
+ postPk: primaryKey({ columns: [t.id] }),
113
+ postScopeIdx: index('post_scope_idx').on(t.workspaceId, t.status, t.createdAt),
114
+ postSlugUniq: uniqueIndex('post_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
115
+ }))
116
+
117
+ export const post_category = pgTable('post_category', {
118
+ ...systemCols(),
119
+ description: text('description'),
120
+ parent: text('parent'),
121
+ position: numeric('position'),
122
+ }, t => ({
123
+ post_categoryPk: primaryKey({ columns: [t.id] }),
124
+ post_categoryScopeIdx: index('post_category_scope_idx').on(t.workspaceId, t.status, t.createdAt),
125
+ post_categorySlugUniq: uniqueIndex('post_category_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
126
+ }))
127
+
128
+ export const showcase = pgTable('showcase', {
129
+ ...systemCols(),
130
+ brandColor: text('brandColor'),
131
+ keywords: text('keywords').array(),
132
+ opensAt: text('opensAt'),
133
+ embedSnippet: text('embedSnippet'),
134
+ location: jsonb('location'),
135
+ price: numeric('price'),
136
+ score: numeric('score'),
137
+ image_variant: text('image_variant'),
138
+ }, t => ({
139
+ showcasePk: primaryKey({ columns: [t.id] }),
140
+ showcaseScopeIdx: index('showcase_scope_idx').on(t.workspaceId, t.status, t.createdAt),
141
+ showcaseSlugUniq: uniqueIndex('showcase_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
142
+ showcaseKeywordsIdx: index('showcase_keywords_idx').using('gin', t.keywords),
143
+ }))
144
+
145
+ export const test = pgTable('test', {
146
+ ...systemCols(),
147
+ text_22: text('text_22'),
148
+ slug2: text('slug2'),
149
+ textarea: text('textarea'),
150
+ richtext: jsonb('richtext'),
151
+ number: numeric('number'),
152
+ boolean: boolean('boolean'),
153
+ date: timestamp('date', { withTimezone: true }),
154
+ select: text('select'),
155
+ media: text('media'),
156
+ relation: text('relation').array(),
157
+ textarea3: text('textarea3'),
158
+ boolean2: boolean('boolean2'),
159
+ object: jsonb('object'),
160
+ dynamic_builder: jsonb('dynamic_builder'),
161
+ textarea2: text('textarea2'),
162
+ }, t => ({
163
+ testPk: primaryKey({ columns: [t.id] }),
164
+ testScopeIdx: index('test_scope_idx').on(t.workspaceId, t.status, t.createdAt),
165
+ testSlugUniq: uniqueIndex('test_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
166
+ }))
167
+
168
+ export const studio_instance = pgTable('studio_instance', {
169
+ ...systemCols(),
170
+ placement: text('placement'),
171
+ description: text('description'),
172
+ blocks: jsonb('blocks'),
173
+ }, t => ({
174
+ studio_instancePk: primaryKey({ columns: [t.id] }),
175
+ studio_instanceScopeIdx: index('studio_instance_scope_idx').on(t.workspaceId, t.status, t.createdAt),
176
+ studio_instanceSlugUniq: uniqueIndex('studio_instance_slug_uniq').on(t.slug).where(sql`slug IS NOT NULL`),
177
+ }))
178
+
179
+
180
+ export const post_i18n = pgTable('post_i18n', {
181
+ objectId: text('object_id').notNull(),
182
+
183
+ locale: text('locale').notNull(),
184
+ __title: text('__title'),
185
+ subtitle: text('subtitle'),
186
+ body: jsonb('body'),
187
+ tags: text('tags').array(),
188
+ excerpt: text('excerpt'),
189
+ bingo: text('bingo').array(),
190
+ metadata: jsonb('metadata'),
191
+ faqs: jsonb('faqs'),
192
+ sections: jsonb('sections'),
193
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
194
+ }, t => ({
195
+ postI18nPk: primaryKey({ columns: [t.objectId, t.locale] }),
196
+ }))
197
+
198
+ export const showcase_i18n = pgTable('showcase_i18n', {
199
+ objectId: text('object_id').notNull(),
200
+
201
+ locale: text('locale').notNull(),
202
+ __title: text('__title'),
203
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
204
+ }, t => ({
205
+ showcaseI18nPk: primaryKey({ columns: [t.objectId, t.locale] }),
206
+ }))
207
+
208
+ export const test_i18n = pgTable('test_i18n', {
209
+ objectId: text('object_id').notNull(),
210
+
211
+ locale: text('locale').notNull(),
212
+ __title: text('__title'),
213
+ select: text('select'),
214
+ dynamic_builder: jsonb('dynamic_builder'),
215
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
216
+ }, t => ({
217
+ testI18nPk: primaryKey({ columns: [t.objectId, t.locale] }),
218
+ }))
219
+
220
+ export const studio_instance_i18n = pgTable('studio_instance_i18n', {
221
+ objectId: text('object_id').notNull(),
222
+
223
+ locale: text('locale').notNull(),
224
+ __title: text('__title'),
225
+ blocks: jsonb('blocks'),
226
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
227
+ }, t => ({
228
+ studio_instanceI18nPk: primaryKey({ columns: [t.objectId, t.locale] }),
229
+ }))
230
+
231
+
232
+ export const contentTables = {
233
+ author,
234
+ coupon,
235
+ page,
236
+ pincode,
237
+ pincode_city,
238
+ post,
239
+ post_category,
240
+ showcase,
241
+ test,
242
+ studio_instance,
243
+ } as const
244
+
245
+ export const contentI18nTables = {
246
+ post: post_i18n,
247
+ showcase: showcase_i18n,
248
+ test: test_i18n,
249
+ studio_instance: studio_instance_i18n,
250
+ } as const
251
+
252
+ export type ContentTableName = keyof typeof contentTables
253
+ export type ContentTable = typeof contentTables[ContentTableName]
254
+
255
+ export const contentTablesMeta = {
256
+ "format": 1,
257
+ "workspaceId": "wsp_local_dev",
258
+ "fingerprint": "c6532a81:2064036",
259
+ "types": [
260
+ "author",
261
+ "coupon",
262
+ "page",
263
+ "pincode",
264
+ "pincode_city",
265
+ "post",
266
+ "post_category",
267
+ "showcase",
268
+ "studio_instance",
269
+ "test"
270
+ ]
271
+ } as const
@@ -0,0 +1,18 @@
1
+ // DO NOT EDIT - generated by tangen push
2
+ // Re-run `tangen push` to regenerate after custom-table changes.
3
+
4
+ import { customTablePg } from '@tangen/db/schema-kit'
5
+
6
+ export const customer_feedback = customTablePg({"name":"customer_feedback","columns":{"email":{"kind":"text","nullable":false},"rating":{"kind":"number","nullable":false},"comment":{"kind":"text","nullable":true},"createdAt":{"kind":"timestamp","nullable":true,"defaultNow":true}}})
7
+
8
+ export const coupons = customTablePg({"name":"coupons","columns":{"code":{"kind":"text","nullable":false},"kind":{"kind":"text","nullable":false},"value":{"kind":"number","nullable":false},"active":{"kind":"boolean","nullable":true,"defaultValue":true},"startsAt":{"kind":"timestamp","nullable":true},"endsAt":{"kind":"timestamp","nullable":true},"maxUses":{"kind":"number","nullable":true},"createdAt":{"kind":"timestamp","nullable":true,"defaultNow":true}}})
9
+
10
+ export const coupon_usages = customTablePg({"name":"coupon_usages","columns":{"code":{"kind":"text","nullable":false},"orderAmount":{"kind":"number","nullable":false},"discount":{"kind":"number","nullable":false},"usedAt":{"kind":"timestamp","nullable":true,"defaultNow":true}}})
11
+
12
+ export const customTables = {
13
+ customer_feedback,
14
+ coupons,
15
+ coupon_usages,
16
+ } as const
17
+
18
+ export type CustomTableName = keyof typeof customTables
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * File Name: index.ts
3
+ * Purpose: Public entry point for the @tangen/db package — re-exports the database
4
+ * client and all system schema tables. This barrel is the published
5
+ * surface and must stay project-independent.
6
+ *
7
+ * Author: Raghunath J
8
+ * License: MIT
9
+ *
10
+ * Dependencies/Notes: Project-generated content tables are NOT exported here —
11
+ * import them from '@tangen/db/content-tables' (repo-only subpath;
12
+ * regenerated by `tangen push`).
13
+ */
14
+ export * from './client'
15
+ export * from './schema'
16
+ export * from './ensure-system-schema'
17
+ export * from './extension-migrations'
18
+ export * from './extension-install-state'
19
+ export * from './knowledge-schema'
20
+ export * from './content-ddl'
21
+ export * from './cache-keys'
22
+ export * from './instrument'
23
+ export * from './prepare-policy'
24
+ export * from './sealed'