@crombolo/data 0.0.0-stage → 0.9.1
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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/package.json +31 -4
- package/src/change-report.ts +166 -0
- package/src/cluster.ts +108 -0
- package/src/database.ts +121 -0
- package/src/dialect.ts +128 -0
- package/src/index.ts +60 -0
- package/src/locks.ts +103 -0
- package/src/maintenance.ts +51 -0
- package/src/migrations.ts +431 -0
- package/src/node-ref.ts +107 -0
- package/src/pending.ts +25 -0
- package/src/repositories/bundles.ts +191 -0
- package/src/repositories/content-types.ts +831 -0
- package/src/repositories/data-types.ts +222 -0
- package/src/repositories/dictionary.ts +259 -0
- package/src/repositories/documents.ts +2389 -0
- package/src/repositories/domains.ts +108 -0
- package/src/repositories/folders.ts +120 -0
- package/src/repositories/form-entries.ts +314 -0
- package/src/repositories/form-workflows.ts +213 -0
- package/src/repositories/languages.ts +108 -0
- package/src/repositories/log-searches.ts +33 -0
- package/src/repositories/members.ts +427 -0
- package/src/repositories/nodes.ts +361 -0
- package/src/repositories/notifications.ts +54 -0
- package/src/repositories/public-access.ts +187 -0
- package/src/repositories/redirects.ts +274 -0
- package/src/repositories/references.ts +162 -0
- package/src/repositories/tags.ts +93 -0
- package/src/repositories/templates.ts +114 -0
- package/src/repositories/transfer-runs.ts +190 -0
- package/src/repositories/user-data.ts +90 -0
- package/src/repositories/users.ts +898 -0
- package/src/schema/assistant.ts +74 -0
- package/src/schema/built-in-content-types.ts +106 -0
- package/src/schema/built-in-media-types.ts +335 -0
- package/src/schema/built-in-member-types.ts +61 -0
- package/src/schema/bundle-components.ts +49 -0
- package/src/schema/bundles-rename.ts +32 -0
- package/src/schema/change-report.ts +46 -0
- package/src/schema/client-id-prefix.ts +43 -0
- package/src/schema/content-editing.ts +50 -0
- package/src/schema/content-seed.ts +586 -0
- package/src/schema/content.ts +263 -0
- package/src/schema/created-packages.ts +56 -0
- package/src/schema/elements.ts +62 -0
- package/src/schema/form-permission-prefix.ts +50 -0
- package/src/schema/form-permissions.ts +26 -0
- package/src/schema/form-workflows.ts +54 -0
- package/src/schema/forms.ts +63 -0
- package/src/schema/group-permissions.ts +20 -0
- package/src/schema/identity.ts +176 -0
- package/src/schema/log-viewer.ts +68 -0
- package/src/schema/member-property-type.ts +30 -0
- package/src/schema/members.ts +97 -0
- package/src/schema/plan.ts +73 -0
- package/src/schema/public-access.ts +56 -0
- package/src/schema/recycle-bin.ts +21 -0
- package/src/schema/redirects.ts +57 -0
- package/src/schema/rich-text-ui.ts +34 -0
- package/src/schema/seed.ts +298 -0
- package/src/schema/server-role.ts +21 -0
- package/src/schema/since-version.ts +26 -0
- package/src/schema/transfer-runs.ts +71 -0
- package/src/schema/upgrade-report.ts +43 -0
- package/src/schema/users-localisation.ts +85 -0
- package/src/schema/values.ts +108 -0
- package/src/schema-ops.ts +35 -0
- package/src/schema-state.ts +196 -0
- package/README.md +0 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema state a database is at, and the rule that only a node at the
|
|
3
|
+
* current state may write.
|
|
4
|
+
*
|
|
5
|
+
* `(version, revision)` is a deployment's identity: `version` is the human
|
|
6
|
+
* value in `schema/schema.toml`, `revision` a monotonic number supplied at
|
|
7
|
+
* deploy time. See docs/10-packaging-and-upgrades.md.
|
|
8
|
+
*/
|
|
9
|
+
import type { Db } from './database.ts'
|
|
10
|
+
import { DbDate } from './dialect.ts'
|
|
11
|
+
import { parseWritesPaused, WRITES_PAUSED_KEY, type WritesPaused } from './maintenance.ts'
|
|
12
|
+
|
|
13
|
+
export interface SchemaStateRow {
|
|
14
|
+
id: number
|
|
15
|
+
version: string
|
|
16
|
+
revision: string
|
|
17
|
+
hash: string | null
|
|
18
|
+
status: 'prepared' | 'current'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NodeSchemaState {
|
|
22
|
+
version: string
|
|
23
|
+
revision: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The state a node runs at until schema-as-code supplies one: the install baseline. */
|
|
27
|
+
export const BASELINE_NODE_STATE: NodeSchemaState = { version: '0', revision: '0' }
|
|
28
|
+
|
|
29
|
+
function segments(value: string): number[] {
|
|
30
|
+
return value.split(/[.+-]/).map((part) => {
|
|
31
|
+
const n = Number(part)
|
|
32
|
+
return Number.isFinite(n) ? n : 0
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Numeric per-segment comparison of dotted versions; -1, 0 or 1. */
|
|
37
|
+
export function compareVersions(a: string, b: string): number {
|
|
38
|
+
const sa = segments(a)
|
|
39
|
+
const sb = segments(b)
|
|
40
|
+
const length = Math.max(sa.length, sb.length)
|
|
41
|
+
for (let i = 0; i < length; i++) {
|
|
42
|
+
const da = sa[i] ?? 0
|
|
43
|
+
const db = sb[i] ?? 0
|
|
44
|
+
if (da !== db) return da < db ? -1 : 1
|
|
45
|
+
}
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function compareStates(a: NodeSchemaState, b: NodeSchemaState): number {
|
|
50
|
+
return compareVersions(a.version, b.version) || compareVersions(a.revision, b.revision)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class WriteRejectedError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
readonly node: NodeSchemaState,
|
|
56
|
+
readonly database: NodeSchemaState,
|
|
57
|
+
) {
|
|
58
|
+
super(
|
|
59
|
+
`This server is at schema ${node.version}+${node.revision} but the database is at ` +
|
|
60
|
+
`${database.version}+${database.revision}; writes are refused until it is upgraded.`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Editing is paused cluster-wide by an operator; a 409 like any other refused write. */
|
|
66
|
+
export class WritesPausedError extends WriteRejectedError {
|
|
67
|
+
constructor(
|
|
68
|
+
node: NodeSchemaState,
|
|
69
|
+
database: NodeSchemaState,
|
|
70
|
+
readonly paused: WritesPaused,
|
|
71
|
+
) {
|
|
72
|
+
super(node, database)
|
|
73
|
+
this.message = paused.reason
|
|
74
|
+
? `Editing is paused: ${paused.reason}. Changes are refused until it resumes.`
|
|
75
|
+
: 'Editing is paused. Changes are refused until it resumes.'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The latest current state. Takes a shared lock where the engine has one, so an upgrade cannot slip between the check and the write. */
|
|
80
|
+
export async function currentSchemaState(db: Db, lock = false): Promise<SchemaStateRow> {
|
|
81
|
+
const suffix = lock ? db.dialect.forShare : ''
|
|
82
|
+
const rows = await db.query(
|
|
83
|
+
`SELECT id, version, revision, hash, status FROM schema_state
|
|
84
|
+
WHERE status = 'current' ORDER BY id DESC LIMIT 1 ${suffix}`,
|
|
85
|
+
)
|
|
86
|
+
const row = rows[0]
|
|
87
|
+
if (!row) throw new Error('schema_state has no current row; the database is not installed.')
|
|
88
|
+
return {
|
|
89
|
+
id: Number(row.id),
|
|
90
|
+
version: String(row.version),
|
|
91
|
+
revision: String(row.revision),
|
|
92
|
+
hash: (row.hash as string | null) ?? null,
|
|
93
|
+
status: String(row.status) as 'prepared' | 'current',
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Called inside every write transaction. Returns the state to stamp on what is
|
|
99
|
+
* written, or throws when the node is behind the database.
|
|
100
|
+
*/
|
|
101
|
+
export async function assertNodeMayWrite(db: Db, node: NodeSchemaState): Promise<SchemaStateRow> {
|
|
102
|
+
const current = await currentSchemaState(db, true)
|
|
103
|
+
if (compareStates(node, current) < 0) throw new WriteRejectedError(node, current)
|
|
104
|
+
const rows = await db.query<{ value: string | null }>(
|
|
105
|
+
'SELECT value FROM key_value WHERE key = ?',
|
|
106
|
+
[WRITES_PAUSED_KEY],
|
|
107
|
+
)
|
|
108
|
+
const paused = parseWritesPaused(rows[0]?.value)
|
|
109
|
+
if (paused) throw new WritesPausedError(node, current, paused)
|
|
110
|
+
return current
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface AdvanceOptions {
|
|
114
|
+
version: string
|
|
115
|
+
revision: string
|
|
116
|
+
hash?: string
|
|
117
|
+
status: 'prepared' | 'current'
|
|
118
|
+
by?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Appends a schema state row. Under `FOR UPDATE` on the previous current row, so it serialises against writers. */
|
|
122
|
+
export async function appendSchemaState(db: Db, options: AdvanceOptions): Promise<SchemaStateRow> {
|
|
123
|
+
await db.query(
|
|
124
|
+
`SELECT id FROM schema_state WHERE status = 'current' ORDER BY id DESC LIMIT 1 ${db.dialect.forUpdate}`,
|
|
125
|
+
)
|
|
126
|
+
await db.exec(
|
|
127
|
+
`INSERT INTO schema_state (version, revision, hash, status, synced_at, synced_by)
|
|
128
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
129
|
+
[
|
|
130
|
+
options.version,
|
|
131
|
+
options.revision,
|
|
132
|
+
options.hash ?? null,
|
|
133
|
+
options.status,
|
|
134
|
+
DbDate.toDb(new Date()),
|
|
135
|
+
options.by ?? null,
|
|
136
|
+
],
|
|
137
|
+
)
|
|
138
|
+
const rows = await db.query(
|
|
139
|
+
'SELECT id, version, revision, hash, status FROM schema_state ORDER BY id DESC LIMIT 1',
|
|
140
|
+
)
|
|
141
|
+
const row = rows[0] as Record<string, unknown>
|
|
142
|
+
return {
|
|
143
|
+
id: Number(row.id),
|
|
144
|
+
version: String(row.version),
|
|
145
|
+
revision: String(row.revision),
|
|
146
|
+
hash: (row.hash as string | null) ?? null,
|
|
147
|
+
status: String(row.status) as 'prepared' | 'current',
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Key write-back changes the files after they were hashed; the state records the files as they now are. */
|
|
152
|
+
export async function updateSchemaStateHash(db: Db, id: number, hash: string): Promise<void> {
|
|
153
|
+
await db.exec('UPDATE schema_state SET hash = ? WHERE id = ?', [hash, id])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Every state row, oldest first. The table is small: one row per deployment. */
|
|
157
|
+
export async function allSchemaStates(db: Db): Promise<SchemaStateRow[]> {
|
|
158
|
+
const rows = await db.query(
|
|
159
|
+
'SELECT id, version, revision, hash, status FROM schema_state ORDER BY id',
|
|
160
|
+
)
|
|
161
|
+
return rows.map((row) => ({
|
|
162
|
+
id: Number(row.id),
|
|
163
|
+
version: String(row.version),
|
|
164
|
+
revision: String(row.revision),
|
|
165
|
+
hash: (row.hash as string | null) ?? null,
|
|
166
|
+
status: String(row.status) as 'prepared' | 'current',
|
|
167
|
+
}))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The newest prepared state, if an early fix has created one. */
|
|
171
|
+
export async function latestPreparedState(db: Db): Promise<SchemaStateRow | undefined> {
|
|
172
|
+
const states = await allSchemaStates(db)
|
|
173
|
+
return states.filter((s) => s.status === 'prepared').at(-1)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The highest state id a node may read as-of: the newest row at or below its
|
|
178
|
+
* own `(version, revision)`. Undefined when every row is visible to it.
|
|
179
|
+
*/
|
|
180
|
+
export async function visibleStateIdFor(
|
|
181
|
+
db: Db,
|
|
182
|
+
node: NodeSchemaState,
|
|
183
|
+
): Promise<number | undefined> {
|
|
184
|
+
const states = await allSchemaStates(db)
|
|
185
|
+
if (states.every((s) => compareStates(s, node) <= 0)) return undefined
|
|
186
|
+
const visible = states.filter((s) => compareStates(s, node) <= 0)
|
|
187
|
+
return visible.at(-1)?.id ?? 0
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Marks a prepared state current — the cut-over. */
|
|
191
|
+
export async function makeStateCurrent(db: Db, id: number): Promise<void> {
|
|
192
|
+
await db.query(
|
|
193
|
+
`SELECT id FROM schema_state WHERE status = 'current' ORDER BY id DESC LIMIT 1 ${db.dialect.forUpdate}`,
|
|
194
|
+
)
|
|
195
|
+
await db.exec("UPDATE schema_state SET status = 'current' WHERE id = ?", [id])
|
|
196
|
+
}
|
package/README.md
DELETED