@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/package.json +31 -4
  4. package/src/change-report.ts +166 -0
  5. package/src/cluster.ts +108 -0
  6. package/src/database.ts +121 -0
  7. package/src/dialect.ts +128 -0
  8. package/src/index.ts +60 -0
  9. package/src/locks.ts +103 -0
  10. package/src/maintenance.ts +51 -0
  11. package/src/migrations.ts +431 -0
  12. package/src/node-ref.ts +107 -0
  13. package/src/pending.ts +25 -0
  14. package/src/repositories/bundles.ts +191 -0
  15. package/src/repositories/content-types.ts +831 -0
  16. package/src/repositories/data-types.ts +222 -0
  17. package/src/repositories/dictionary.ts +259 -0
  18. package/src/repositories/documents.ts +2389 -0
  19. package/src/repositories/domains.ts +108 -0
  20. package/src/repositories/folders.ts +120 -0
  21. package/src/repositories/form-entries.ts +314 -0
  22. package/src/repositories/form-workflows.ts +213 -0
  23. package/src/repositories/languages.ts +108 -0
  24. package/src/repositories/log-searches.ts +33 -0
  25. package/src/repositories/members.ts +427 -0
  26. package/src/repositories/nodes.ts +361 -0
  27. package/src/repositories/notifications.ts +54 -0
  28. package/src/repositories/public-access.ts +187 -0
  29. package/src/repositories/redirects.ts +274 -0
  30. package/src/repositories/references.ts +162 -0
  31. package/src/repositories/tags.ts +93 -0
  32. package/src/repositories/templates.ts +114 -0
  33. package/src/repositories/transfer-runs.ts +190 -0
  34. package/src/repositories/user-data.ts +90 -0
  35. package/src/repositories/users.ts +898 -0
  36. package/src/schema/assistant.ts +74 -0
  37. package/src/schema/built-in-content-types.ts +106 -0
  38. package/src/schema/built-in-media-types.ts +335 -0
  39. package/src/schema/built-in-member-types.ts +61 -0
  40. package/src/schema/bundle-components.ts +49 -0
  41. package/src/schema/bundles-rename.ts +32 -0
  42. package/src/schema/change-report.ts +46 -0
  43. package/src/schema/client-id-prefix.ts +43 -0
  44. package/src/schema/content-editing.ts +50 -0
  45. package/src/schema/content-seed.ts +586 -0
  46. package/src/schema/content.ts +263 -0
  47. package/src/schema/created-packages.ts +56 -0
  48. package/src/schema/elements.ts +62 -0
  49. package/src/schema/form-permission-prefix.ts +50 -0
  50. package/src/schema/form-permissions.ts +26 -0
  51. package/src/schema/form-workflows.ts +54 -0
  52. package/src/schema/forms.ts +63 -0
  53. package/src/schema/group-permissions.ts +20 -0
  54. package/src/schema/identity.ts +176 -0
  55. package/src/schema/log-viewer.ts +68 -0
  56. package/src/schema/member-property-type.ts +30 -0
  57. package/src/schema/members.ts +97 -0
  58. package/src/schema/plan.ts +73 -0
  59. package/src/schema/public-access.ts +56 -0
  60. package/src/schema/recycle-bin.ts +21 -0
  61. package/src/schema/redirects.ts +57 -0
  62. package/src/schema/rich-text-ui.ts +34 -0
  63. package/src/schema/seed.ts +298 -0
  64. package/src/schema/server-role.ts +21 -0
  65. package/src/schema/since-version.ts +26 -0
  66. package/src/schema/transfer-runs.ts +71 -0
  67. package/src/schema/upgrade-report.ts +43 -0
  68. package/src/schema/users-localisation.ts +85 -0
  69. package/src/schema/values.ts +108 -0
  70. package/src/schema-ops.ts +35 -0
  71. package/src/schema-state.ts +196 -0
  72. package/README.md +0 -3
@@ -0,0 +1,190 @@
1
+ /**
2
+ * What content imports have been applied here, and what each one touched.
3
+ *
4
+ * The rows exist so that reverting is a lookup rather than a guess: each change
5
+ * remembers the event its node was at beforehand and the published event it was
6
+ * serving, which together *are* the previously-live state.
7
+ */
8
+ import type { Db } from '../database.ts'
9
+ import { DbDate } from '../dialect.ts'
10
+
11
+ export type TransferDirection = 'import' | 'revert'
12
+ export type TransferRunStatus = 'applied' | 'reverted' | 'failed'
13
+ /**
14
+ * What a run did to one node.
15
+ *
16
+ * `unchanged` is recorded too: a run that decided to do nothing still owns the
17
+ * node. `recycle` is what a *revert* does to a node the import created, and it
18
+ * is recorded so that reverting the revert knows to bring it back out of the
19
+ * bin rather than only restoring its values.
20
+ */
21
+ export type TransferAction = 'create' | 'update' | 'unchanged' | 'skip' | 'recycle'
22
+
23
+ export interface TransferRun {
24
+ id: string
25
+ bundleId: string
26
+ bundleLabel: string | null
27
+ direction: TransferDirection
28
+ revertsRunId: string | null
29
+ schemaStateId: number
30
+ startedAt: Date
31
+ finishedAt: Date | null
32
+ appliedBy: string | null
33
+ nodeCount: number
34
+ status: TransferRunStatus
35
+ }
36
+
37
+ export interface TransferChange {
38
+ id: number
39
+ runId: string
40
+ nodeKey: string
41
+ kind: string
42
+ action: TransferAction
43
+ /** The node's latest event before this run, or null when the run created it. */
44
+ beforeEventId: number | null
45
+ /** The event it was serving before this run, or null if it was not published. */
46
+ beforePublishedEventId: number | null
47
+ /** The event this run appended, when it appended one. */
48
+ eventId: number | null
49
+ }
50
+
51
+ export class TransferRunRepository {
52
+ #db: Db
53
+ constructor(db: Db) {
54
+ this.#db = db
55
+ }
56
+
57
+ async start(run: Omit<TransferRun, 'finishedAt' | 'status'>): Promise<void> {
58
+ await this.#db.exec(
59
+ `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')`,
62
+ [
63
+ run.id,
64
+ run.bundleId,
65
+ run.bundleLabel,
66
+ run.direction,
67
+ run.revertsRunId,
68
+ run.schemaStateId,
69
+ DbDate.toDb(run.startedAt),
70
+ run.appliedBy,
71
+ run.nodeCount,
72
+ ],
73
+ )
74
+ }
75
+
76
+ async finish(id: string, nodeCount: number): Promise<void> {
77
+ await this.#db.exec(
78
+ 'UPDATE content_transfer_run SET finished_at = ?, node_count = ? WHERE id = ?',
79
+ [DbDate.toDb(new Date()), nodeCount, id],
80
+ )
81
+ }
82
+
83
+ async setStatus(id: string, status: TransferRunStatus): Promise<void> {
84
+ await this.#db.exec('UPDATE content_transfer_run SET status = ? WHERE id = ?', [status, id])
85
+ }
86
+
87
+ async record(change: Omit<TransferChange, 'id'>): Promise<void> {
88
+ await this.#db.exec(
89
+ `INSERT INTO content_transfer_change
90
+ (run_id, node_key, kind, action, before_event_id, before_published_event_id, event_id)
91
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
92
+ [
93
+ change.runId,
94
+ change.nodeKey,
95
+ change.kind,
96
+ change.action,
97
+ change.beforeEventId,
98
+ change.beforePublishedEventId,
99
+ change.eventId,
100
+ ],
101
+ )
102
+ }
103
+
104
+ async byId(id: string): Promise<TransferRun | undefined> {
105
+ const rows = await this.#db.query('SELECT * FROM content_transfer_run WHERE id = ?', [id])
106
+ return rows[0] ? mapRun(rows[0]) : undefined
107
+ }
108
+
109
+ /**
110
+ * The import of this bundle that still stands here, if there is one. A run
111
+ * that was reverted does not count: the content is gone, so importing again
112
+ * is the right answer. This is what makes `start --bundle` import once.
113
+ */
114
+ async appliedFor(bundleId: string): Promise<TransferRun | undefined> {
115
+ const rows = await this.#db.query(
116
+ `SELECT * FROM content_transfer_run
117
+ WHERE bundle_id = ? AND direction = 'import' AND status = 'applied'
118
+ ORDER BY started_at DESC, id DESC LIMIT 1`,
119
+ [bundleId],
120
+ )
121
+ return rows[0] ? mapRun(rows[0]) : undefined
122
+ }
123
+
124
+ /** Runs, newest first. */
125
+ async recent(limit = 20): Promise<TransferRun[]> {
126
+ const rows = await this.#db.query(
127
+ 'SELECT * FROM content_transfer_run ORDER BY started_at DESC, id DESC LIMIT ?',
128
+ [limit],
129
+ )
130
+ return rows.map(mapRun)
131
+ }
132
+
133
+ async changes(runId: string): Promise<TransferChange[]> {
134
+ const rows = await this.#db.query(
135
+ 'SELECT * FROM content_transfer_change WHERE run_id = ? ORDER BY id',
136
+ [runId],
137
+ )
138
+ return rows.map(mapChange)
139
+ }
140
+
141
+ /**
142
+ * Runs after this one that touched any of the same nodes. Reverting while one
143
+ * of these stands would quietly undo its work too, so revert refuses.
144
+ */
145
+ async laterRunsTouching(runId: string): Promise<string[]> {
146
+ const rows = await this.#db.query<{ id: string }>(
147
+ `SELECT DISTINCT later.id
148
+ FROM content_transfer_change mine
149
+ JOIN content_transfer_change theirs ON theirs.node_key = mine.node_key
150
+ JOIN content_transfer_run later ON later.id = theirs.run_id
151
+ JOIN content_transfer_run self ON self.id = mine.run_id
152
+ WHERE mine.run_id = ?
153
+ AND theirs.run_id <> mine.run_id
154
+ AND later.started_at >= self.started_at
155
+ AND later.status = 'applied'`,
156
+ [runId],
157
+ )
158
+ return rows.map((row) => String(row.id))
159
+ }
160
+ }
161
+
162
+ function mapRun(row: Record<string, unknown>): TransferRun {
163
+ return {
164
+ id: String(row.id),
165
+ bundleId: String(row.bundle_id),
166
+ bundleLabel: (row.bundle_label as string | null) ?? null,
167
+ direction: String(row.direction) as TransferDirection,
168
+ revertsRunId: (row.reverts_run_id as string | null) ?? null,
169
+ schemaStateId: Number(row.schema_state_id),
170
+ startedAt: DbDate.fromDb(row.started_at) ?? new Date(0),
171
+ finishedAt: DbDate.fromDb(row.finished_at) ?? null,
172
+ appliedBy: (row.applied_by as string | null) ?? null,
173
+ nodeCount: Number(row.node_count),
174
+ status: String(row.status) as TransferRunStatus,
175
+ }
176
+ }
177
+
178
+ function mapChange(row: Record<string, unknown>): TransferChange {
179
+ const int = (value: unknown) => (value === null || value === undefined ? null : Number(value))
180
+ return {
181
+ id: Number(row.id),
182
+ runId: String(row.run_id),
183
+ nodeKey: String(row.node_key),
184
+ kind: String(row.kind),
185
+ action: String(row.action) as TransferAction,
186
+ beforeEventId: int(row.before_event_id),
187
+ beforePublishedEventId: int(row.before_published_event_id),
188
+ eventId: int(row.event_id),
189
+ }
190
+ }
@@ -0,0 +1,90 @@
1
+ /** Per-user key/value data the backoffice keeps between sessions (Umbraco's `umbracoUserData`). */
2
+ import { normaliseUuid } from '@crombolo/core'
3
+ import type { Db } from '../database.ts'
4
+
5
+ export interface UserDataEntry {
6
+ key: string
7
+ group: string
8
+ identifier: string
9
+ value: string
10
+ }
11
+
12
+ export type UserDataResult = 'Success' | 'NotFound' | 'AlreadyExists'
13
+
14
+ const map = (row: Record<string, unknown>): UserDataEntry => ({
15
+ key: normaliseUuid(String(row.key)),
16
+ group: String(row.data_group),
17
+ identifier: String(row.identifier),
18
+ value: String(row.value),
19
+ })
20
+
21
+ export class UserDataRepository {
22
+ #db: Db
23
+
24
+ constructor(db: Db) {
25
+ this.#db = db
26
+ }
27
+
28
+ async list(
29
+ userId: number,
30
+ filter: { groups?: readonly string[]; identifiers?: readonly string[] },
31
+ skip: number,
32
+ take: number,
33
+ ): Promise<{ total: number; items: UserDataEntry[] }> {
34
+ const where = ['user_id = ?']
35
+ const params: unknown[] = [userId]
36
+ if (filter.groups?.length) {
37
+ where.push(`data_group IN (${filter.groups.map(() => '?').join(', ')})`)
38
+ params.push(...filter.groups)
39
+ }
40
+ if (filter.identifiers?.length) {
41
+ where.push(`identifier IN (${filter.identifiers.map(() => '?').join(', ')})`)
42
+ params.push(...filter.identifiers)
43
+ }
44
+ const rows = await this.#db.query(
45
+ `SELECT key, data_group, identifier, value FROM user_data WHERE ${where.join(' AND ')}
46
+ ORDER BY data_group, identifier`,
47
+ params,
48
+ )
49
+ return { total: rows.length, items: rows.slice(skip, skip + take).map(map) }
50
+ }
51
+
52
+ async get(userId: number, key: string): Promise<UserDataEntry | undefined> {
53
+ const rows = await this.#db.query(
54
+ 'SELECT key, data_group, identifier, value FROM user_data WHERE user_id = ? AND key = ?',
55
+ [userId, normaliseUuid(key)],
56
+ )
57
+ return rows[0] ? map(rows[0]) : undefined
58
+ }
59
+
60
+ async create(userId: number, entry: UserDataEntry): Promise<UserDataResult> {
61
+ const clash = await this.#db.query(
62
+ 'SELECT 1 FROM user_data WHERE key = ? OR (user_id = ? AND data_group = ? AND identifier = ?)',
63
+ [normaliseUuid(entry.key), userId, entry.group, entry.identifier],
64
+ )
65
+ if (clash.length > 0) return 'AlreadyExists'
66
+ await this.#db.exec(
67
+ 'INSERT INTO user_data (key, user_id, data_group, identifier, value) VALUES (?, ?, ?, ?, ?)',
68
+ [normaliseUuid(entry.key), userId, entry.group, entry.identifier, entry.value],
69
+ )
70
+ return 'Success'
71
+ }
72
+
73
+ async update(userId: number, entry: UserDataEntry): Promise<UserDataResult> {
74
+ if (!(await this.get(userId, entry.key))) return 'NotFound'
75
+ await this.#db.exec(
76
+ 'UPDATE user_data SET data_group = ?, identifier = ?, value = ? WHERE user_id = ? AND key = ?',
77
+ [entry.group, entry.identifier, entry.value, userId, normaliseUuid(entry.key)],
78
+ )
79
+ return 'Success'
80
+ }
81
+
82
+ async delete(userId: number, key: string): Promise<UserDataResult> {
83
+ if (!(await this.get(userId, key))) return 'NotFound'
84
+ await this.#db.exec('DELETE FROM user_data WHERE user_id = ? AND key = ?', [
85
+ userId,
86
+ normaliseUuid(key),
87
+ ])
88
+ return 'Success'
89
+ }
90
+ }