@crombolo/data 0.9.1 → 0.10.0
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/package.json +2 -2
- package/src/bundle-tables/builder.ts +264 -0
- package/src/bundle-tables/capability.ts +377 -0
- package/src/bundle-tables/chain.ts +105 -0
- package/src/bundle-tables/ddl.ts +124 -0
- package/src/bundle-tables/guard.ts +116 -0
- package/src/bundle-tables/index.ts +7 -0
- package/src/bundle-tables/names.ts +58 -0
- package/src/bundle-tables/schema.ts +485 -0
- package/src/cluster.ts +1 -1
- package/src/dialect.ts +8 -0
- package/src/index.ts +3 -0
- package/src/migrations.ts +147 -31
- package/src/pending.ts +1 -1
- package/src/repositories/bundles.ts +36 -4
- package/src/repositories/documents.ts +1 -1
- package/src/repositories/form-entries.ts +1 -1
- package/src/repositories/form-workflows.ts +1 -1
- package/src/repositories/transfer-runs.ts +20 -2
- package/src/schema/bundle-components.ts +1 -1
- package/src/schema/bundle-package.ts +27 -0
- package/src/schema/bundles-rename.ts +1 -1
- package/src/schema/content.ts +2 -2
- package/src/schema/created-packages.ts +1 -1
- package/src/schema/form-permissions.ts +1 -1
- package/src/schema/form-workflows.ts +1 -1
- package/src/schema/forms.ts +1 -1
- package/src/schema/identity.ts +1 -1
- package/src/schema/plan.ts +4 -0
- package/src/schema/transfer-run-package.ts +27 -0
- package/src/schema/upgrade-report.ts +1 -1
- package/src/schema-state.ts +1 -1
package/src/migrations.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* version number and not a list of applied migrations: the plan is a chain of
|
|
4
4
|
* transitions walked forward to its final state. That design handles concurrent
|
|
5
5
|
* development branches far better than sequence numbers, so we keep it — and
|
|
6
|
-
* harden it (docs/
|
|
6
|
+
* harden it (docs/packaging-and-upgrades.md): a ledger of every step that
|
|
7
7
|
* ran, expand/contract declared and linted, a recorded plan, and an explicit
|
|
8
8
|
* refusal when the database is newer than the code.
|
|
9
9
|
*/
|
|
@@ -29,7 +29,24 @@ export interface Migration {
|
|
|
29
29
|
release?: string
|
|
30
30
|
/** For a contract: the name of the expand migration it removes. */
|
|
31
31
|
contracts?: string
|
|
32
|
+
/** Recorded in the ledger, and compared when the plan verifies what ran. Defaults to `name`. */
|
|
33
|
+
checksum?: string
|
|
32
34
|
up(db: Db): Promise<void>
|
|
35
|
+
/**
|
|
36
|
+
* Writes data once `up` has run, in the same transaction. `planUpgrade` cannot
|
|
37
|
+
* predict what it writes, so a step that has one is reported as unplannable.
|
|
38
|
+
*/
|
|
39
|
+
backfill?(db: Db): Promise<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Where a chain keeps its state, and what it calls itself in the ledger. */
|
|
43
|
+
export interface ChainOptions {
|
|
44
|
+
/** The `key_value` key holding the chain's state. */
|
|
45
|
+
stateKey?: string
|
|
46
|
+
/** The state before the first step. */
|
|
47
|
+
initialState?: string
|
|
48
|
+
/** A bundle's id, for a bundle's chain; absent for core's. */
|
|
49
|
+
bundle?: string
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
function compareRelease(a: string, b: string): number {
|
|
@@ -71,10 +88,16 @@ export function isMajorRelease(release: string): boolean {
|
|
|
71
88
|
|
|
72
89
|
export class MigrationPlan {
|
|
73
90
|
readonly migrations: readonly Migration[]
|
|
91
|
+
readonly stateKey: string
|
|
92
|
+
readonly initialState: string
|
|
93
|
+
readonly bundle: string | undefined
|
|
74
94
|
|
|
75
|
-
constructor(migrations: readonly Migration[]) {
|
|
95
|
+
constructor(migrations: readonly Migration[], options: ChainOptions = {}) {
|
|
76
96
|
this.migrations = migrations
|
|
77
|
-
|
|
97
|
+
this.stateKey = options.stateKey ?? MIGRATION_STATE_KEY
|
|
98
|
+
this.initialState = options.initialState ?? INITIAL_STATE
|
|
99
|
+
this.bundle = options.bundle
|
|
100
|
+
let expected = this.initialState
|
|
78
101
|
const seen = new Map<string, Migration>()
|
|
79
102
|
for (const migration of migrations) {
|
|
80
103
|
if (migration.from !== expected) {
|
|
@@ -111,7 +134,14 @@ export class MigrationPlan {
|
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
get finalState(): string {
|
|
114
|
-
return this.migrations.at(-1)?.to ??
|
|
137
|
+
return this.migrations.at(-1)?.to ?? this.initialState
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The steps that have run when the chain is at a state; undefined when the state is not in it. */
|
|
141
|
+
appliedAt(state: string): Migration[] | undefined {
|
|
142
|
+
if (state === this.initialState) return []
|
|
143
|
+
const index = this.migrations.findIndex((m) => m.to === state)
|
|
144
|
+
return index < 0 ? undefined : this.migrations.slice(0, index + 1)
|
|
115
145
|
}
|
|
116
146
|
|
|
117
147
|
/** The steps still to run from a state; empty when current. Undefined when the state is not in the plan. */
|
|
@@ -123,7 +153,11 @@ export class MigrationPlan {
|
|
|
123
153
|
}
|
|
124
154
|
}
|
|
125
155
|
|
|
156
|
+
/** Connections whose ledger tables are known to exist with every column. */
|
|
157
|
+
const ensured = new WeakSet<Db>()
|
|
158
|
+
|
|
126
159
|
async function ensureTables(db: Db): Promise<void> {
|
|
160
|
+
if (ensured.has(db)) return
|
|
127
161
|
const t = db.dialect.types
|
|
128
162
|
await db.exec(
|
|
129
163
|
`CREATE TABLE IF NOT EXISTS key_value (
|
|
@@ -143,36 +177,62 @@ async function ensureTables(db: Db): Promise<void> {
|
|
|
143
177
|
duration_ms ${t.integer} NOT NULL,
|
|
144
178
|
checksum ${t.varchar(64)},
|
|
145
179
|
applied_by ${t.varchar(255)},
|
|
146
|
-
note ${t.text}
|
|
180
|
+
note ${t.text},
|
|
181
|
+
bundle ${t.varchar(64)}
|
|
147
182
|
)`,
|
|
148
183
|
)
|
|
184
|
+
// A ledger from before bundles could own tables has no `bundle` column. It is
|
|
185
|
+
// added here, where the table is, rather than by a core migration: the ledger
|
|
186
|
+
// has to be able to record a bundle's step whatever state core is at.
|
|
187
|
+
if (!(await hasColumn(db, 'migration_history', 'bundle')))
|
|
188
|
+
await db.exec(`ALTER TABLE migration_history ADD COLUMN bundle ${t.varchar(64)}`)
|
|
189
|
+
ensured.add(db)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function hasColumn(db: Db, table: string, column: string): Promise<boolean> {
|
|
193
|
+
const rows =
|
|
194
|
+
db.dialect.name === 'sqlite'
|
|
195
|
+
? await db.query<{ name: string }>(`SELECT name FROM pragma_table_info('${table}')`)
|
|
196
|
+
: await db.query<{ name: string }>(
|
|
197
|
+
'SELECT column_name AS name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ?',
|
|
198
|
+
[table],
|
|
199
|
+
)
|
|
200
|
+
return rows.some((row) => row.name === column)
|
|
149
201
|
}
|
|
150
202
|
|
|
151
|
-
|
|
203
|
+
/** A chain's state: core's unless a plan says otherwise. */
|
|
204
|
+
export async function readState(
|
|
205
|
+
db: Db,
|
|
206
|
+
chain: Pick<MigrationPlan, 'stateKey' | 'initialState'> = {
|
|
207
|
+
stateKey: MIGRATION_STATE_KEY,
|
|
208
|
+
initialState: INITIAL_STATE,
|
|
209
|
+
},
|
|
210
|
+
): Promise<string> {
|
|
152
211
|
await ensureTables(db)
|
|
153
212
|
const rows = await db.query<{ value: string }>('SELECT value FROM key_value WHERE key = ?', [
|
|
154
|
-
|
|
213
|
+
chain.stateKey,
|
|
155
214
|
])
|
|
156
|
-
return rows[0]?.value ??
|
|
215
|
+
return rows[0]?.value ?? chain.initialState
|
|
157
216
|
}
|
|
158
217
|
|
|
159
|
-
async function writeState(db: Db, state: string): Promise<void> {
|
|
218
|
+
async function writeState(db: Db, state: string, key = MIGRATION_STATE_KEY): Promise<void> {
|
|
160
219
|
const now = db.dialect.now()
|
|
161
|
-
await db.query(`UPDATE key_value SET value = ?, update_date = ${now} WHERE key = ?`, [
|
|
162
|
-
|
|
163
|
-
MIGRATION_STATE_KEY,
|
|
164
|
-
])
|
|
165
|
-
const rows = await db.query<{ value: string }>('SELECT value FROM key_value WHERE key = ?', [
|
|
166
|
-
MIGRATION_STATE_KEY,
|
|
167
|
-
])
|
|
220
|
+
await db.query(`UPDATE key_value SET value = ?, update_date = ${now} WHERE key = ?`, [state, key])
|
|
221
|
+
const rows = await db.query<{ value: string }>('SELECT value FROM key_value WHERE key = ?', [key])
|
|
168
222
|
if (rows.length === 0) {
|
|
169
223
|
await db.exec(
|
|
170
224
|
`INSERT INTO key_value (key, value, update_date) VALUES (${db.dialect.placeholder(1)}, ${db.dialect.placeholder(2)}, ${now})`,
|
|
171
|
-
[
|
|
225
|
+
[key, state],
|
|
172
226
|
)
|
|
173
227
|
}
|
|
174
228
|
}
|
|
175
229
|
|
|
230
|
+
/** Forgets a setting entirely, as a purged bundle's state is forgotten. */
|
|
231
|
+
export async function deleteKeyValue(db: Db, key: string): Promise<void> {
|
|
232
|
+
await ensureTables(db)
|
|
233
|
+
await db.exec('DELETE FROM key_value WHERE key = ?', [key])
|
|
234
|
+
}
|
|
235
|
+
|
|
176
236
|
export interface LedgerEntry {
|
|
177
237
|
name: string
|
|
178
238
|
/** 'expand' | 'contract' for framework steps; 'value' for a value migration; 'upgrade' for a cut-over. */
|
|
@@ -183,6 +243,8 @@ export interface LedgerEntry {
|
|
|
183
243
|
checksum?: string | null
|
|
184
244
|
appliedBy?: string | null
|
|
185
245
|
note?: string | null
|
|
246
|
+
/** The bundle whose chain this step belongs to; null for core. */
|
|
247
|
+
bundle?: string | null
|
|
186
248
|
}
|
|
187
249
|
|
|
188
250
|
/**
|
|
@@ -226,8 +288,8 @@ export async function ensureKeyValue(db: Db, key: string, create: () => string):
|
|
|
226
288
|
export async function recordInLedger(db: Db, entry: LedgerEntry): Promise<void> {
|
|
227
289
|
await ensureTables(db)
|
|
228
290
|
await db.exec(
|
|
229
|
-
`INSERT INTO migration_history (name, kind, from_state, to_state, applied_at, duration_ms, checksum, applied_by, note)
|
|
230
|
-
VALUES (?, ?, ?, ?, ${db.dialect.now()}, ?, ?, ?, ?)`,
|
|
291
|
+
`INSERT INTO migration_history (name, kind, from_state, to_state, applied_at, duration_ms, checksum, applied_by, note, bundle)
|
|
292
|
+
VALUES (?, ?, ?, ?, ${db.dialect.now()}, ?, ?, ?, ?, ?)`,
|
|
231
293
|
[
|
|
232
294
|
entry.name,
|
|
233
295
|
entry.kind,
|
|
@@ -237,6 +299,7 @@ export async function recordInLedger(db: Db, entry: LedgerEntry): Promise<void>
|
|
|
237
299
|
entry.checksum ?? null,
|
|
238
300
|
entry.appliedBy ?? null,
|
|
239
301
|
entry.note ?? null,
|
|
302
|
+
entry.bundle ?? null,
|
|
240
303
|
],
|
|
241
304
|
)
|
|
242
305
|
}
|
|
@@ -248,10 +311,14 @@ export async function latestLedgerId(db: Db): Promise<number> {
|
|
|
248
311
|
return Number(rows[0]?.n ?? 0)
|
|
249
312
|
}
|
|
250
313
|
|
|
251
|
-
/**
|
|
314
|
+
/**
|
|
315
|
+
* How many contract steps — framework contracts and site purges — were ledgered
|
|
316
|
+
* after a row id. A bundle's contract is not counted: it removes only the
|
|
317
|
+
* bundle's own tables, which no read of core's depends on.
|
|
318
|
+
*/
|
|
252
319
|
export async function contractsLedgeredAfter(db: Db, afterId: number): Promise<number> {
|
|
253
320
|
const rows = await db.query<{ n: number }>(
|
|
254
|
-
"SELECT COUNT(*) AS n FROM migration_history WHERE id > ? AND kind = 'contract'",
|
|
321
|
+
"SELECT COUNT(*) AS n FROM migration_history WHERE id > ? AND kind = 'contract' AND bundle IS NULL",
|
|
255
322
|
[afterId],
|
|
256
323
|
)
|
|
257
324
|
return Number(rows[0]?.n ?? 0)
|
|
@@ -265,7 +332,7 @@ export interface LedgerRow extends LedgerEntry {
|
|
|
265
332
|
export async function readLedger(db: Db): Promise<LedgerRow[]> {
|
|
266
333
|
await ensureTables(db)
|
|
267
334
|
const rows = await db.query(
|
|
268
|
-
'SELECT id, name, kind, from_state, to_state, applied_at, duration_ms, checksum, applied_by, note FROM migration_history ORDER BY id',
|
|
335
|
+
'SELECT id, name, kind, from_state, to_state, applied_at, duration_ms, checksum, applied_by, note, bundle FROM migration_history ORDER BY id',
|
|
269
336
|
)
|
|
270
337
|
return rows.map((row) => ({
|
|
271
338
|
id: Number(row.id),
|
|
@@ -278,6 +345,7 @@ export async function readLedger(db: Db): Promise<LedgerRow[]> {
|
|
|
278
345
|
checksum: (row.checksum as string | null) ?? null,
|
|
279
346
|
appliedBy: (row.applied_by as string | null) ?? null,
|
|
280
347
|
note: (row.note as string | null) ?? null,
|
|
348
|
+
bundle: (row.bundle as string | null) ?? null,
|
|
281
349
|
}))
|
|
282
350
|
}
|
|
283
351
|
|
|
@@ -286,22 +354,66 @@ export class UpgradeStateError extends Error {
|
|
|
286
354
|
constructor(
|
|
287
355
|
readonly databaseState: string,
|
|
288
356
|
readonly codeState: string,
|
|
357
|
+
/** The bundle whose chain it is, when it is not core's. */
|
|
358
|
+
readonly bundle?: string,
|
|
359
|
+
) {
|
|
360
|
+
super(
|
|
361
|
+
bundle
|
|
362
|
+
? `The database holds the bundle "${bundle}"'s tables at state ${databaseState}, which this version of the bundle (its steps end at ${codeState}) does not know. ` +
|
|
363
|
+
'A newer release of the bundle wrote them; install that release, or restore the backup taken before the upgrade.'
|
|
364
|
+
: `The database is at migration state ${databaseState}, which this version of crombolo (plan ends at ${codeState}) does not know. ` +
|
|
365
|
+
'It was upgraded by a newer version; run that version, or restore the backup taken before the upgrade.',
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** A bundle step changed after it ran here: what is in the database is not what the code says made it. */
|
|
371
|
+
export class BundleStepEditedError extends Error {
|
|
372
|
+
constructor(
|
|
373
|
+
readonly bundle: string,
|
|
374
|
+
readonly step: string,
|
|
289
375
|
) {
|
|
290
376
|
super(
|
|
291
|
-
`The
|
|
292
|
-
'
|
|
377
|
+
`The bundle "${bundle}"'s step "${step}" is not the step that ran on this database: it was edited after it shipped. ` +
|
|
378
|
+
'A shipped step is never changed; put the change in a new step.',
|
|
293
379
|
)
|
|
294
380
|
}
|
|
295
381
|
}
|
|
296
382
|
|
|
297
383
|
/** Steps pending on this database, in order. Throws when the database is ahead of the code. */
|
|
298
384
|
export async function pendingMigrations(db: Db, plan: MigrationPlan): Promise<Migration[]> {
|
|
299
|
-
const state = await readState(db)
|
|
385
|
+
const state = await readState(db, plan)
|
|
300
386
|
const pending = plan.pendingFrom(state)
|
|
301
|
-
if (!pending) throw new UpgradeStateError(state, plan.finalState)
|
|
387
|
+
if (!pending) throw new UpgradeStateError(state, plan.finalState, plan.bundle)
|
|
302
388
|
return pending
|
|
303
389
|
}
|
|
304
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Refuses a bundle chain whose applied steps no longer match the code. Only the
|
|
393
|
+
* rows since the bundle was last purged count: a purge starts its chain again.
|
|
394
|
+
*/
|
|
395
|
+
async function verifyApplied(db: Db, plan: MigrationPlan, state: string): Promise<void> {
|
|
396
|
+
if (!plan.bundle) return
|
|
397
|
+
const applied = plan.appliedAt(state) ?? []
|
|
398
|
+
if (applied.length === 0) return
|
|
399
|
+
const rows = await db.query<{ name: string; kind: string; checksum: string | null }>(
|
|
400
|
+
'SELECT name, kind, checksum FROM migration_history WHERE bundle = ? ORDER BY id',
|
|
401
|
+
[plan.bundle],
|
|
402
|
+
)
|
|
403
|
+
const purged = rows.map((row) => row.kind).lastIndexOf('purge')
|
|
404
|
+
const ran = new Map(
|
|
405
|
+
rows
|
|
406
|
+
.slice(purged + 1)
|
|
407
|
+
.filter((row) => row.kind === 'expand' || row.kind === 'contract')
|
|
408
|
+
.map((row) => [row.name, row.checksum]),
|
|
409
|
+
)
|
|
410
|
+
for (const step of applied) {
|
|
411
|
+
const recorded = ran.get(step.name)
|
|
412
|
+
if (recorded !== undefined && recorded !== (step.checksum ?? step.name))
|
|
413
|
+
throw new BundleStepEditedError(plan.bundle, step.name)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
305
417
|
export interface MigrateResult {
|
|
306
418
|
from: string
|
|
307
419
|
to: string
|
|
@@ -320,27 +432,30 @@ export async function migrate(
|
|
|
320
432
|
options: MigrateOptions = {},
|
|
321
433
|
): Promise<MigrateResult> {
|
|
322
434
|
return db.locks.withLock(Locks.KeyValues, async () => {
|
|
323
|
-
const from = await readState(db)
|
|
435
|
+
const from = await readState(db, plan)
|
|
324
436
|
let state = from
|
|
325
437
|
const applied: string[] = []
|
|
326
438
|
const pending = plan.pendingFrom(state)
|
|
327
|
-
if (!pending) throw new UpgradeStateError(state, plan.finalState)
|
|
439
|
+
if (!pending) throw new UpgradeStateError(state, plan.finalState, plan.bundle)
|
|
440
|
+
await verifyApplied(db, plan, state)
|
|
328
441
|
for (const next of pending) {
|
|
329
442
|
const started = performance.now()
|
|
330
443
|
await db.transaction(async (tx) => {
|
|
331
444
|
await next.up(tx)
|
|
445
|
+
await next.backfill?.(tx)
|
|
332
446
|
})
|
|
333
447
|
state = next.to
|
|
334
|
-
await writeState(db, state)
|
|
448
|
+
await writeState(db, state, plan.stateKey)
|
|
335
449
|
await recordInLedger(db, {
|
|
336
450
|
name: next.name,
|
|
337
451
|
kind: next.kind ?? 'expand',
|
|
338
452
|
fromState: next.from,
|
|
339
453
|
toState: next.to,
|
|
340
454
|
durationMs: Math.round(performance.now() - started),
|
|
341
|
-
checksum: next.name,
|
|
455
|
+
checksum: next.checksum ?? next.name,
|
|
342
456
|
appliedBy: options.by ?? null,
|
|
343
457
|
note: next.release ? `release ${next.release}` : null,
|
|
458
|
+
bundle: plan.bundle ?? null,
|
|
344
459
|
})
|
|
345
460
|
applied.push(next.name)
|
|
346
461
|
}
|
|
@@ -351,7 +466,7 @@ export async function migrate(
|
|
|
351
466
|
/** A fresh install stamps the final state so no migrations then run. */
|
|
352
467
|
export async function stampFinalState(db: Db, plan: MigrationPlan): Promise<void> {
|
|
353
468
|
await ensureTables(db)
|
|
354
|
-
await writeState(db, plan.finalState)
|
|
469
|
+
await writeState(db, plan.finalState, plan.stateKey)
|
|
355
470
|
}
|
|
356
471
|
|
|
357
472
|
// ---------------------------------------------------------------- the plan
|
|
@@ -421,6 +536,7 @@ export async function planUpgrade(db: Db, plan: MigrationPlan): Promise<PlannedS
|
|
|
421
536
|
try {
|
|
422
537
|
await migration.up(recorder)
|
|
423
538
|
step.statements = recorder.statements
|
|
539
|
+
if (migration.backfill) step.unplannable = 'backfills data, which only running it can show'
|
|
424
540
|
} catch (error) {
|
|
425
541
|
if (error instanceof UnplannableError) step.unplannable = error.message
|
|
426
542
|
else throw error
|
package/src/pending.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An element is pending on a node when the deployment that created it is newer
|
|
3
3
|
* than the node, or its file says `since` a version the node has not reached.
|
|
4
|
-
* The one place the rule lives. docs/
|
|
4
|
+
* The one place the rule lives. docs/schema-as-code.md, "since".
|
|
5
5
|
*/
|
|
6
6
|
import { compareStates, compareVersions, type NodeSchemaState } from './schema-state.ts'
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The bundle definitions: what a bundle would contain, not a built bundle.
|
|
3
|
-
* `docs/
|
|
3
|
+
* `docs/bundles.md`.
|
|
4
4
|
*
|
|
5
5
|
* The selection lists travel as JSON text in single columns. A malformed list
|
|
6
6
|
* reads back as empty rather than throwing: a definition whose row was edited
|
|
@@ -26,12 +26,21 @@ export interface Bundle {
|
|
|
26
26
|
scripts: string[]
|
|
27
27
|
languages: string[]
|
|
28
28
|
dictionaryItems: string[]
|
|
29
|
+
/**
|
|
30
|
+
* The npm package the bundle is built as. Not in Umbraco's definition, so the
|
|
31
|
+
* contract never writes these; null until someone sets them.
|
|
32
|
+
*/
|
|
33
|
+
packageName: string | null
|
|
34
|
+
packageVersion: string | null
|
|
29
35
|
createDate: Date
|
|
30
36
|
updateDate: Date
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/** A definition to store; `id` is assigned when it is new, the dates always. */
|
|
34
|
-
export type BundleInput = Omit<
|
|
40
|
+
export type BundleInput = Omit<
|
|
41
|
+
Bundle,
|
|
42
|
+
'id' | 'createDate' | 'updateDate' | 'packageName' | 'packageVersion'
|
|
43
|
+
>
|
|
35
44
|
|
|
36
45
|
interface Row {
|
|
37
46
|
id: string
|
|
@@ -49,6 +58,8 @@ interface Row {
|
|
|
49
58
|
scripts: string
|
|
50
59
|
languages: string
|
|
51
60
|
dictionary_items: string
|
|
61
|
+
package_name: string | null
|
|
62
|
+
package_version: string | null
|
|
52
63
|
create_date: unknown
|
|
53
64
|
update_date: unknown
|
|
54
65
|
}
|
|
@@ -56,7 +67,8 @@ interface Row {
|
|
|
56
67
|
const SELECT = `SELECT id, name, content_node_id, content_load_child_nodes,
|
|
57
68
|
media_load_child_nodes, media_ids, element_ids, content_types,
|
|
58
69
|
media_types, data_types, components, stylesheets,
|
|
59
|
-
scripts, languages, dictionary_items,
|
|
70
|
+
scripts, languages, dictionary_items, package_name, package_version,
|
|
71
|
+
create_date, update_date
|
|
60
72
|
FROM bundle`
|
|
61
73
|
|
|
62
74
|
const list = (value: string | null): string[] => {
|
|
@@ -87,6 +99,8 @@ const hydrate = (row: Row): Bundle => ({
|
|
|
87
99
|
scripts: list(row.scripts),
|
|
88
100
|
languages: list(row.languages),
|
|
89
101
|
dictionaryItems: list(row.dictionary_items),
|
|
102
|
+
packageName: row.package_name ?? null,
|
|
103
|
+
packageVersion: row.package_version ?? null,
|
|
90
104
|
createDate: DbDate.fromDb(row.create_date) ?? new Date(0),
|
|
91
105
|
updateDate: DbDate.fromDb(row.update_date) ?? new Date(0),
|
|
92
106
|
})
|
|
@@ -152,7 +166,14 @@ export class BundleRepository {
|
|
|
152
166
|
DbDate.toDb(now),
|
|
153
167
|
],
|
|
154
168
|
)
|
|
155
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
...input,
|
|
171
|
+
id,
|
|
172
|
+
packageName: null,
|
|
173
|
+
packageVersion: null,
|
|
174
|
+
createDate: now,
|
|
175
|
+
updateDate: now,
|
|
176
|
+
}
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
async update(id: string, input: BundleInput): Promise<void> {
|
|
@@ -185,6 +206,17 @@ export class BundleRepository {
|
|
|
185
206
|
)
|
|
186
207
|
}
|
|
187
208
|
|
|
209
|
+
/** The npm package a definition is built as; null clears a value back to its default. */
|
|
210
|
+
async setPackage(
|
|
211
|
+
id: string,
|
|
212
|
+
pkg: { name: string | null; version: string | null },
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
await this.#db.exec(
|
|
215
|
+
'UPDATE bundle SET package_name = ?, package_version = ?, update_date = ? WHERE id = ?',
|
|
216
|
+
[pkg.name, pkg.version, DbDate.toDb(new Date()), id],
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
188
220
|
async delete(id: string): Promise<void> {
|
|
189
221
|
await this.#db.exec('DELETE FROM bundle WHERE id = ?', [id])
|
|
190
222
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* tagged with the event that wrote it and the schema state the writer was at.
|
|
6
6
|
* `content_version` rows are events (save | publish | rollback | migrate).
|
|
7
7
|
* Publish is an event and a pointer, not a copy; rollback appends the old
|
|
8
|
-
* values under a new event. See docs/
|
|
8
|
+
* values under a new event. See docs/data-model.md.
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
11
|
type DocumentAggregate,
|
|
@@ -24,6 +24,9 @@ export interface TransferRun {
|
|
|
24
24
|
id: string
|
|
25
25
|
bundleId: string
|
|
26
26
|
bundleLabel: string | null
|
|
27
|
+
/** The npm package the content arrived in, and its version. */
|
|
28
|
+
packageName: string | null
|
|
29
|
+
packageVersion: string | null
|
|
27
30
|
direction: TransferDirection
|
|
28
31
|
revertsRunId: string | null
|
|
29
32
|
schemaStateId: number
|
|
@@ -57,12 +60,14 @@ export class TransferRunRepository {
|
|
|
57
60
|
async start(run: Omit<TransferRun, 'finishedAt' | 'status'>): Promise<void> {
|
|
58
61
|
await this.#db.exec(
|
|
59
62
|
`INSERT INTO content_transfer_run
|
|
60
|
-
(id, bundle_id, bundle_label, direction, reverts_run_id, schema_state_id, started_at, applied_by, node_count, status)
|
|
61
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'applied')`,
|
|
63
|
+
(id, bundle_id, bundle_label, package_name, package_version, direction, reverts_run_id, schema_state_id, started_at, applied_by, node_count, status)
|
|
64
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'applied')`,
|
|
62
65
|
[
|
|
63
66
|
run.id,
|
|
64
67
|
run.bundleId,
|
|
65
68
|
run.bundleLabel,
|
|
69
|
+
run.packageName,
|
|
70
|
+
run.packageVersion,
|
|
66
71
|
run.direction,
|
|
67
72
|
run.revertsRunId,
|
|
68
73
|
run.schemaStateId,
|
|
@@ -121,6 +126,17 @@ export class TransferRunRepository {
|
|
|
121
126
|
return rows[0] ? mapRun(rows[0]) : undefined
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
/** The latest standing import of a package's content, whichever version brought it. */
|
|
130
|
+
async appliedForPackage(packageName: string): Promise<TransferRun | undefined> {
|
|
131
|
+
const rows = await this.#db.query(
|
|
132
|
+
`SELECT * FROM content_transfer_run
|
|
133
|
+
WHERE package_name = ? AND direction = 'import' AND status = 'applied'
|
|
134
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`,
|
|
135
|
+
[packageName],
|
|
136
|
+
)
|
|
137
|
+
return rows[0] ? mapRun(rows[0]) : undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
/** Runs, newest first. */
|
|
125
141
|
async recent(limit = 20): Promise<TransferRun[]> {
|
|
126
142
|
const rows = await this.#db.query(
|
|
@@ -164,6 +180,8 @@ function mapRun(row: Record<string, unknown>): TransferRun {
|
|
|
164
180
|
id: String(row.id),
|
|
165
181
|
bundleId: String(row.bundle_id),
|
|
166
182
|
bundleLabel: (row.bundle_label as string | null) ?? null,
|
|
183
|
+
packageName: (row.package_name as string | null) ?? null,
|
|
184
|
+
packageVersion: (row.package_version as string | null) ?? null,
|
|
167
185
|
direction: String(row.direction) as TransferDirection,
|
|
168
186
|
revertsRunId: (row.reverts_run_id as string | null) ?? null,
|
|
169
187
|
schemaStateId: Number(row.schema_state_id),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Migration 028 — a bundle's `templates` and `partial_views` become `components`.
|
|
3
3
|
*
|
|
4
|
-
* There is one root and one kind of file on disk now (`docs/
|
|
4
|
+
* There is one root and one kind of file on disk now (`docs/rendering.md`),
|
|
5
5
|
* so a bundle definition that still kept two lists was recording a distinction
|
|
6
6
|
* the rest of the system no longer makes. The two are merged rather than one
|
|
7
7
|
* dropped: both held component aliases, and which tree the backoffice picked
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 029 — a created bundle records the npm package it is written as.
|
|
3
|
+
*
|
|
4
|
+
* Every bundle is an npm package (`docs/bundles.md`), so the Created view's
|
|
5
|
+
* download needs a package name and version. Umbraco's package definition has
|
|
6
|
+
* neither, so they live here, beside the columns the contract does name. Both
|
|
7
|
+
* are nullable: an unset name or version is derived when the bundle is built.
|
|
8
|
+
*/
|
|
9
|
+
import type { Db } from '../database.ts'
|
|
10
|
+
import type { Migration } from '../migrations.ts'
|
|
11
|
+
import { schemaOps } from '../schema-ops.ts'
|
|
12
|
+
import { STATE_BUNDLE_COMPONENTS } from './bundle-components.ts'
|
|
13
|
+
|
|
14
|
+
export const STATE_BUNDLE_PACKAGE = '{6f1d4a8e-0029-4c5e-9a17-3d8b2e6f4c29}'
|
|
15
|
+
|
|
16
|
+
export const bundlePackageMigration: Migration = {
|
|
17
|
+
from: STATE_BUNDLE_COMPONENTS,
|
|
18
|
+
to: STATE_BUNDLE_PACKAGE,
|
|
19
|
+
name: 'AddBundlePackageNameAndVersion',
|
|
20
|
+
kind: 'expand',
|
|
21
|
+
release: '0.10.0',
|
|
22
|
+
async up(db: Db) {
|
|
23
|
+
const ops = schemaOps(db)
|
|
24
|
+
await ops.addColumn('bundle', 'package_name', db.dialect.types.varchar(214))
|
|
25
|
+
await ops.addColumn('bundle', 'package_version', db.dialect.types.varchar(64))
|
|
26
|
+
},
|
|
27
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The table was named after Umbraco's `umbracoCreatedPackageSchema`, which it
|
|
5
5
|
* mirrors column for column. What it holds is a bundle definition
|
|
6
|
-
* (`docs/
|
|
6
|
+
* (`docs/bundles.md`), and the backoffice now says so, so the table does too.
|
|
7
7
|
*
|
|
8
8
|
* A rename rather than an edit to migration 022: 022 has already run on every
|
|
9
9
|
* database that exists, and rewriting it would leave those sites with a table
|
package/src/schema/content.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Migration 002 — the content schema.
|
|
3
3
|
*
|
|
4
4
|
* `node` is the universal polymorphic tree; every other table here is a facet of
|
|
5
|
-
* it. See docs/
|
|
5
|
+
* it. See docs/data-model.md for the model and why it is kept.
|
|
6
6
|
*/
|
|
7
7
|
import type { Db } from '../database.ts'
|
|
8
8
|
import type { Migration } from '../migrations.ts'
|
|
@@ -164,7 +164,7 @@ export const contentMigration: Migration = {
|
|
|
164
164
|
)`,
|
|
165
165
|
)
|
|
166
166
|
|
|
167
|
-
// ---- documents and versions (
|
|
167
|
+
// ---- documents and versions (story 005 uses these) ----
|
|
168
168
|
|
|
169
169
|
await db.exec(
|
|
170
170
|
`CREATE TABLE content (
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Migration 022 — the created-package definitions the Packages section builds.
|
|
3
3
|
*
|
|
4
4
|
* A definition, never an artifact: the zip is built when it is downloaded
|
|
5
|
-
* (`docs/
|
|
5
|
+
* (`docs/bundles.md`), so there are no bytes here to go stale when a
|
|
6
6
|
* content type the package carries is edited afterwards.
|
|
7
7
|
*
|
|
8
8
|
* The selection lists are JSON text rather than a row each. They are read and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Migration 025 — the form permission verbs, for databases seeded before they
|
|
3
|
-
* existed. `docs/
|
|
3
|
+
* existed. `docs/forms.md`.
|
|
4
4
|
*
|
|
5
5
|
* The same shape as migration 007: granting what the seed now grants, so an
|
|
6
6
|
* upgraded site does not have a Forms section nobody can open. Idempotent, and
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Migration 024 — the queue a form's workflows run from. `docs/
|
|
2
|
+
* Migration 024 — the queue a form's workflows run from. `docs/forms.md`.
|
|
3
3
|
*
|
|
4
4
|
* A submission is stored and *then* its workflows run, so a mail server being
|
|
5
5
|
* down never loses an entry. That means a row per workflow per submission,
|
package/src/schema/forms.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Migration 023 — form entries. `docs/
|
|
2
|
+
* Migration 023 — form entries. `docs/forms.md`.
|
|
3
3
|
*
|
|
4
4
|
* The definition is a file, so there is no form table and nothing to point at:
|
|
5
5
|
* an entry carries `form_key`, the UUID from `schema/forms/*.toml`, and keeps
|
package/src/schema/identity.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Migration 001 — back-office identity.
|
|
3
3
|
*
|
|
4
|
-
* Table and column names follow docs/
|
|
4
|
+
* Table and column names follow docs/data-model.md: Umbraco's relational
|
|
5
5
|
* model with snake_case names. `user` is reserved in Postgres, hence
|
|
6
6
|
* `user_account`.
|
|
7
7
|
*/
|
package/src/schema/plan.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { MigrationPlan } from '../migrations.ts'
|
|
15
15
|
import { assistantMigration } from './assistant.ts'
|
|
16
16
|
import { bundleComponentsMigration } from './bundle-components.ts'
|
|
17
|
+
import { bundlePackageMigration } from './bundle-package.ts'
|
|
17
18
|
import { bundlesRenameMigration } from './bundles-rename.ts'
|
|
18
19
|
import { changeReportMigration } from './change-report.ts'
|
|
19
20
|
import { clientIdPrefixMigration } from './client-id-prefix.ts'
|
|
@@ -36,6 +37,7 @@ import { redirectsMigration } from './redirects.ts'
|
|
|
36
37
|
import { richTextUiMigration } from './rich-text-ui.ts'
|
|
37
38
|
import { serverRoleMigration } from './server-role.ts'
|
|
38
39
|
import { sinceVersionMigration } from './since-version.ts'
|
|
40
|
+
import { transferRunPackageMigration } from './transfer-run-package.ts'
|
|
39
41
|
import { transferRunsMigration } from './transfer-runs.ts'
|
|
40
42
|
import { upgradeReportMigration } from './upgrade-report.ts'
|
|
41
43
|
import { usersLocalisationMigration } from './users-localisation.ts'
|
|
@@ -70,4 +72,6 @@ export const cromboloPlan = new MigrationPlan([
|
|
|
70
72
|
formPermissionPrefixMigration,
|
|
71
73
|
bundlesRenameMigration,
|
|
72
74
|
bundleComponentsMigration,
|
|
75
|
+
bundlePackageMigration,
|
|
76
|
+
transferRunPackageMigration,
|
|
73
77
|
])
|