@tangen/db 0.0.0-stage → 0.1.55
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.
- package/README.md +73 -2
- package/drizzle/0000_flimsy_shotgun.sql +339 -0
- package/drizzle/0001_app_data.sql +11 -0
- package/drizzle/0002_add_indexes.sql +36 -0
- package/drizzle/0003_extension_data_rename.sql +3 -0
- package/drizzle/0004_extension_data_published_globals.sql +12 -0
- package/drizzle/0005_auth.sql +51 -0
- package/drizzle/0006_auth_identities.sql +13 -0
- package/drizzle/0007_ai.sql +18 -0
- package/drizzle/0008_ai_chat_threads.sql +5 -0
- package/drizzle/0009_forms_source_slug.sql +11 -0
- package/drizzle/0010_rbac_roles_status.sql +14 -0
- package/drizzle/0011_jobs_v2.sql +67 -0
- package/drizzle/0012_search_platform.sql +36 -0
- package/drizzle/0013_search_synonym_label.sql +2 -0
- package/drizzle/0014_object_types_disabled.sql +2 -0
- package/drizzle/0015_playground_collections.sql +25 -0
- package/drizzle/0016_catchup_activity_promotion.sql +36 -0
- package/drizzle/0017_fields_disabled.sql +3 -0
- package/drizzle/0018_object_types_plural.sql +1 -0
- package/drizzle/0019_member_profile.sql +2 -0
- package/drizzle/0020_paused_jobs.sql +7 -0
- package/drizzle/0021_form_submission_notes.sql +2 -0
- package/drizzle/0022_paused_jobs_env_reason.sql +4 -0
- package/drizzle/0023_scale_indexes.sql +3 -0
- package/drizzle/0024_migration_runs.sql +24 -0
- package/drizzle/0025_porter_transfer.sql +30 -0
- package/drizzle/0026_cron_timezone.sql +1 -0
- package/drizzle/0027_rate_limit_counters.sql +6 -0
- package/drizzle/0028_realtime_events.sql +10 -0
- package/drizzle/0030_delivery_artifacts.sql +10 -0
- package/drizzle/0031_realtime_presence.sql +8 -0
- package/drizzle/0032_schema_pushes.sql +15 -0
- package/drizzle/0033_studio_design_capability.sql +37 -0
- package/drizzle/0034_object_types_provided_by.sql +12 -0
- package/drizzle/0035_custom_table_owners.sql +24 -0
- package/drizzle/0036_revision_version_unique.sql +42 -0
- package/drizzle/0037_job_queue_dedupe.sql +20 -0
- package/drizzle/0038_search_documents.sql +87 -0
- package/drizzle/0039_job_prune_indexes.sql +25 -0
- package/drizzle/0040_extension_data_object_idx.sql +10 -0
- package/drizzle/0041_knowledge.sql +79 -0
- package/drizzle/0042_schema_pushes_checksum.sql +7 -0
- package/drizzle/0043_guard_adoption.sql +12 -0
- package/drizzle/0044_retention_indexes.sql +7 -0
- package/drizzle/0045_extension_install_per_env.sql +40 -0
- package/drizzle/0046_extension_migrations.sql +18 -0
- package/drizzle/0047_environment_role.sql +25 -0
- package/drizzle/0048_environment_role_adoption.sql +30 -0
- package/drizzle/0049_release_lifecycle.sql +55 -0
- package/drizzle/0050_member_env_membership.sql +21 -0
- package/drizzle/0051_remove_environments.sql +350 -0
- package/drizzle/0052_release_actor_columns.sql +9 -0
- package/drizzle/0053_kernel_event_record.sql +26 -0
- package/drizzle/0054_kernel_run_identity.sql +13 -0
- package/drizzle/0055_kernel_identities.sql +19 -0
- package/drizzle/0056_forms_leave_core.sql +7 -0
- package/drizzle/0057_auth_leave_core.sql +8 -0
- package/drizzle/0058_member_grants.sql +11 -0
- package/drizzle/0059_extension_granted_grants.sql +7 -0
- package/drizzle/0060_connections.sql +21 -0
- package/drizzle/meta/0000_snapshot.json +1692 -0
- package/drizzle/meta/0002_snapshot.json +1935 -0
- package/drizzle/meta/0003_snapshot.json +1935 -0
- package/drizzle/meta/_journal.json +426 -0
- package/package.json +85 -4
- package/src/cache-keys.ts +43 -0
- package/src/client.ts +136 -0
- package/src/content-ddl.ts +70 -0
- package/src/content-tables.ts +110 -0
- package/src/custom-tables-loader.ts +65 -0
- package/src/ensure-system-schema.ts +195 -0
- package/src/extension-install-state.ts +220 -0
- package/src/extension-migrations.ts +270 -0
- package/src/generated/content-tables.ts +271 -0
- package/src/generated/custom-tables.ts +18 -0
- package/src/index.ts +24 -0
- package/src/instrument.ts +139 -0
- package/src/knowledge-schema.ts +232 -0
- package/src/migrate.ts +67 -0
- package/src/prepare-policy.ts +118 -0
- package/src/reconcile.ts +115 -0
- package/src/schema/ai-chats.ts +36 -0
- package/src/schema/ai-settings.ts +24 -0
- package/src/schema/api-keys.ts +37 -0
- package/src/schema/audit-log.ts +54 -0
- package/src/schema/connections.ts +35 -0
- package/src/schema/custom-table-owners.ts +32 -0
- package/src/schema/delivery-artifacts.ts +33 -0
- package/src/schema/entity-tags.ts +33 -0
- package/src/schema/extension-data.ts +38 -0
- package/src/schema/extension-migrations.ts +42 -0
- package/src/schema/fields.ts +37 -0
- package/src/schema/globals.ts +35 -0
- package/src/schema/identities.ts +27 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/jobs.ts +104 -0
- package/src/schema/knowledge.ts +85 -0
- package/src/schema/media.ts +52 -0
- package/src/schema/member-grants.ts +34 -0
- package/src/schema/members.ts +49 -0
- package/src/schema/migration-runs.ts +46 -0
- package/src/schema/object-types.ts +41 -0
- package/src/schema/objects.ts +38 -0
- package/src/schema/paused-jobs.ts +25 -0
- package/src/schema/playground.ts +43 -0
- package/src/schema/porter-transfer.ts +47 -0
- package/src/schema/realtime.ts +45 -0
- package/src/schema/releases.ts +95 -0
- package/src/schema/roles.ts +27 -0
- package/src/schema/saved-views.ts +33 -0
- package/src/schema/schema-pushes.ts +43 -0
- package/src/schema/search-documents.ts +47 -0
- package/src/schema/search-query-log.ts +24 -0
- package/src/schema/search-recommendations.ts +27 -0
- package/src/schema/search-synonyms.ts +23 -0
- package/src/schema/webhook-deliveries.ts +37 -0
- package/src/schema/webhooks.ts +36 -0
- package/src/schema/workspace-activity.ts +35 -0
- package/src/schema/workspace-domains.ts +24 -0
- package/src/schema/workspace-installed-apps.ts +48 -0
- package/src/schema/workspaces.ts +24 -0
- package/src/schema-kit.ts +64 -0
- package/src/sealed.ts +84 -0
- package/src/seed/skeleton.ts +77 -0
- package/src/sql/apply-gin-indexes.ts +31 -0
- 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'
|