@crombolo/data 0.0.0-stage → 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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/package.json +31 -4
  4. package/src/bundle-tables/builder.ts +264 -0
  5. package/src/bundle-tables/capability.ts +377 -0
  6. package/src/bundle-tables/chain.ts +105 -0
  7. package/src/bundle-tables/ddl.ts +124 -0
  8. package/src/bundle-tables/guard.ts +116 -0
  9. package/src/bundle-tables/index.ts +7 -0
  10. package/src/bundle-tables/names.ts +58 -0
  11. package/src/bundle-tables/schema.ts +485 -0
  12. package/src/change-report.ts +166 -0
  13. package/src/cluster.ts +108 -0
  14. package/src/database.ts +121 -0
  15. package/src/dialect.ts +136 -0
  16. package/src/index.ts +63 -0
  17. package/src/locks.ts +103 -0
  18. package/src/maintenance.ts +51 -0
  19. package/src/migrations.ts +547 -0
  20. package/src/node-ref.ts +107 -0
  21. package/src/pending.ts +25 -0
  22. package/src/repositories/bundles.ts +223 -0
  23. package/src/repositories/content-types.ts +831 -0
  24. package/src/repositories/data-types.ts +222 -0
  25. package/src/repositories/dictionary.ts +259 -0
  26. package/src/repositories/documents.ts +2389 -0
  27. package/src/repositories/domains.ts +108 -0
  28. package/src/repositories/folders.ts +120 -0
  29. package/src/repositories/form-entries.ts +314 -0
  30. package/src/repositories/form-workflows.ts +213 -0
  31. package/src/repositories/languages.ts +108 -0
  32. package/src/repositories/log-searches.ts +33 -0
  33. package/src/repositories/members.ts +427 -0
  34. package/src/repositories/nodes.ts +361 -0
  35. package/src/repositories/notifications.ts +54 -0
  36. package/src/repositories/public-access.ts +187 -0
  37. package/src/repositories/redirects.ts +274 -0
  38. package/src/repositories/references.ts +162 -0
  39. package/src/repositories/tags.ts +93 -0
  40. package/src/repositories/templates.ts +114 -0
  41. package/src/repositories/transfer-runs.ts +208 -0
  42. package/src/repositories/user-data.ts +90 -0
  43. package/src/repositories/users.ts +898 -0
  44. package/src/schema/assistant.ts +74 -0
  45. package/src/schema/built-in-content-types.ts +106 -0
  46. package/src/schema/built-in-media-types.ts +335 -0
  47. package/src/schema/built-in-member-types.ts +61 -0
  48. package/src/schema/bundle-components.ts +49 -0
  49. package/src/schema/bundle-package.ts +27 -0
  50. package/src/schema/bundles-rename.ts +32 -0
  51. package/src/schema/change-report.ts +46 -0
  52. package/src/schema/client-id-prefix.ts +43 -0
  53. package/src/schema/content-editing.ts +50 -0
  54. package/src/schema/content-seed.ts +586 -0
  55. package/src/schema/content.ts +263 -0
  56. package/src/schema/created-packages.ts +56 -0
  57. package/src/schema/elements.ts +62 -0
  58. package/src/schema/form-permission-prefix.ts +50 -0
  59. package/src/schema/form-permissions.ts +26 -0
  60. package/src/schema/form-workflows.ts +54 -0
  61. package/src/schema/forms.ts +63 -0
  62. package/src/schema/group-permissions.ts +20 -0
  63. package/src/schema/identity.ts +176 -0
  64. package/src/schema/log-viewer.ts +68 -0
  65. package/src/schema/member-property-type.ts +30 -0
  66. package/src/schema/members.ts +97 -0
  67. package/src/schema/plan.ts +77 -0
  68. package/src/schema/public-access.ts +56 -0
  69. package/src/schema/recycle-bin.ts +21 -0
  70. package/src/schema/redirects.ts +57 -0
  71. package/src/schema/rich-text-ui.ts +34 -0
  72. package/src/schema/seed.ts +298 -0
  73. package/src/schema/server-role.ts +21 -0
  74. package/src/schema/since-version.ts +26 -0
  75. package/src/schema/transfer-run-package.ts +27 -0
  76. package/src/schema/transfer-runs.ts +71 -0
  77. package/src/schema/upgrade-report.ts +43 -0
  78. package/src/schema/users-localisation.ts +85 -0
  79. package/src/schema/values.ts +108 -0
  80. package/src/schema-ops.ts +35 -0
  81. package/src/schema-state.ts +196 -0
  82. package/README.md +0 -3
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Install seed data, mirroring Umbraco's DatabaseDataCreator.
3
+ *
4
+ * Password hashing is injected rather than imported so this package stays free of
5
+ * a dependency on @crombolo/auth.
6
+ */
7
+ import type { Db } from '../database.ts'
8
+ import { DbDate } from '../dialect.ts'
9
+
10
+ const ELEMENT_EDIT = [
11
+ 'Umb.Element.Create',
12
+ 'Umb.Element.Update',
13
+ 'Umb.Element.Delete',
14
+ 'Umb.Element.Move',
15
+ 'Umb.Element.Duplicate',
16
+ 'Umb.Element.Publish',
17
+ 'Umb.Element.Unpublish',
18
+ 'Umb.Element.Read',
19
+ 'Umb.Element.Rollback',
20
+ 'Umb.ElementContainer.Create',
21
+ 'Umb.ElementContainer.Update',
22
+ 'Umb.ElementContainer.Delete',
23
+ 'Umb.ElementContainer.Move',
24
+ 'Umb.ElementContainer.Read',
25
+ ]
26
+
27
+ /**
28
+ * The permission verbs each built-in group is installed with — Umbraco's
29
+ * `DatabaseDataCreator.CreateUserGroup2PermissionData`, verbatim, including the
30
+ * legacy single-character letters it still seeds. The backoffice reads these
31
+ * as the user's fallback permissions; without them it hides every
32
+ * permission-gated action, Create included.
33
+ */
34
+ /** Everything about forms, which only an administrator gets by default. */
35
+ const FORM_FULL = [
36
+ 'Crombolo.Form.Read',
37
+ 'Crombolo.Form.Manage',
38
+ 'Crombolo.FormEntry.Read',
39
+ 'Crombolo.FormEntry.Manage',
40
+ 'Crombolo.FormEntry.Sensitive',
41
+ ] as const
42
+
43
+ export const BUILT_IN_GROUP_PERMISSIONS: Record<string, readonly string[]> = {
44
+ admin: [
45
+ 'Umb.Document.Create',
46
+ 'Umb.Document.Update',
47
+ 'Umb.Document.Delete',
48
+ 'Umb.Document.Move',
49
+ 'Umb.Document.Duplicate',
50
+ 'Umb.Document.Sort',
51
+ 'Umb.Document.Rollback',
52
+ 'Umb.Document.PublicAccess',
53
+ 'Umb.Document.CultureAndHostnames',
54
+ 'Umb.Document.Publish',
55
+ 'Umb.Document.Permissions',
56
+ 'Umb.Document.Unpublish',
57
+ 'Umb.Document.Read',
58
+ 'Umb.Document.CreateBlueprint',
59
+ 'Umb.Document.Notifications',
60
+ ':',
61
+ '5',
62
+ '7',
63
+ 'T',
64
+ 'Umb.Document.PropertyValue.Read',
65
+ 'Umb.Document.PropertyValue.Write',
66
+ ...FORM_FULL,
67
+ ...ELEMENT_EDIT,
68
+ ],
69
+ editor: [
70
+ 'Umb.Document.Create',
71
+ 'Umb.Document.Update',
72
+ 'Umb.Document.Delete',
73
+ 'Umb.Document.Move',
74
+ 'Umb.Document.Duplicate',
75
+ 'Umb.Document.Sort',
76
+ 'Umb.Document.Rollback',
77
+ 'Umb.Document.PublicAccess',
78
+ 'Umb.Document.Publish',
79
+ 'Umb.Document.Unpublish',
80
+ 'Umb.Document.Read',
81
+ 'Umb.Document.CreateBlueprint',
82
+ 'Umb.Document.Notifications',
83
+ ':',
84
+ '5',
85
+ 'T',
86
+ 'Umb.Document.PropertyValue.Read',
87
+ 'Umb.Document.PropertyValue.Write',
88
+ // An editor reads entries and acts on them, but does not design forms and
89
+ // does not see what a definition marks sensitive.
90
+ 'Crombolo.Form.Read',
91
+ 'Crombolo.FormEntry.Read',
92
+ 'Crombolo.FormEntry.Manage',
93
+ ...ELEMENT_EDIT,
94
+ ],
95
+ writer: [
96
+ 'Umb.Document.Create',
97
+ 'Umb.Document.Update',
98
+ 'Umb.Document.Read',
99
+ 'Umb.Document.Notifications',
100
+ ':',
101
+ 'Umb.Document.PropertyValue.Read',
102
+ 'Umb.Document.PropertyValue.Write',
103
+ 'Umb.Element.Create',
104
+ 'Umb.Element.Update',
105
+ 'Umb.Element.Read',
106
+ 'Umb.ElementContainer.Create',
107
+ 'Umb.ElementContainer.Update',
108
+ 'Umb.ElementContainer.Read',
109
+ ],
110
+ // The group that exists only to carry sensitive-data access; it has no
111
+ // sections of its own and is added alongside another group.
112
+ sensitiveData: ['Crombolo.FormEntry.Sensitive'],
113
+ translator: [
114
+ 'Umb.Document.Update',
115
+ 'Umb.Document.Read',
116
+ 'Umb.Document.PropertyValue.Read',
117
+ 'Umb.Document.PropertyValue.Write',
118
+ 'Umb.Element.Update',
119
+ 'Umb.Element.Read',
120
+ 'Umb.ElementContainer.Update',
121
+ 'Umb.ElementContainer.Read',
122
+ ],
123
+ }
124
+
125
+ /**
126
+ * Grants each built-in group its default verbs where it lacks them. Plannable
127
+ * (no reads) and idempotent; used by the seed and by migration 007.
128
+ */
129
+ export async function grantBuiltInPermissions(db: Db): Promise<void> {
130
+ for (const [alias, permissions] of Object.entries(BUILT_IN_GROUP_PERMISSIONS)) {
131
+ for (const permission of permissions) {
132
+ await db.exec(
133
+ `INSERT INTO user_group_permission (user_group_key, permission)
134
+ SELECT g.key, ? FROM user_group g
135
+ WHERE g.alias = ? AND NOT EXISTS (
136
+ SELECT 1 FROM user_group_permission p WHERE p.user_group_key = g.key AND p.permission = ?
137
+ )`,
138
+ [permission, alias, permission],
139
+ )
140
+ }
141
+ }
142
+ }
143
+
144
+ /** Umbraco's five built-in groups, key for key, with their seeded section access. */
145
+ export const BUILT_IN_GROUPS = [
146
+ {
147
+ id: 1,
148
+ key: 'e5e7f6c8-7f9c-4b5b-8d5d-9e1e5a4f7e4d',
149
+ alias: 'admin',
150
+ name: 'Administrators',
151
+ icon: 'icon-medal',
152
+ hasAccessToAllLanguages: true,
153
+ sections: [
154
+ 'content',
155
+ 'packages',
156
+ 'media',
157
+ 'members',
158
+ 'settings',
159
+ 'users',
160
+ 'forms',
161
+ 'translation',
162
+ 'library',
163
+ ],
164
+ },
165
+ {
166
+ id: 2,
167
+ key: '9fc2a16f-528c-46d6-a014-75bf4ec2480c',
168
+ alias: 'writer',
169
+ name: 'Writers',
170
+ icon: 'icon-edit',
171
+ hasAccessToAllLanguages: true,
172
+ sections: ['content', 'library'],
173
+ },
174
+ {
175
+ id: 3,
176
+ key: '44dc260e-b4d4-4dd9-9081-eec5598f1641',
177
+ alias: 'editor',
178
+ name: 'Editors',
179
+ icon: 'icon-tools',
180
+ hasAccessToAllLanguages: true,
181
+ sections: ['content', 'media', 'forms', 'library'],
182
+ },
183
+ {
184
+ id: 4,
185
+ key: 'f2012e4c-d232-4bd1-8eae-4384032d97d8',
186
+ alias: 'translator',
187
+ name: 'Translators',
188
+ icon: 'icon-globe',
189
+ hasAccessToAllLanguages: true,
190
+ sections: ['translation'],
191
+ },
192
+ {
193
+ id: 5,
194
+ key: '8c6ad70f-d307-4e4a-af58-72c2e4e9439d',
195
+ alias: 'sensitiveData',
196
+ name: 'Sensitive data',
197
+ icon: 'icon-lock',
198
+ hasAccessToAllLanguages: false,
199
+ sections: [],
200
+ },
201
+ ] as const
202
+
203
+ /** Umbraco's super user: the installer's administrator, whom only they can see in the user lists. */
204
+ export const SUPER_USER_KEY = '1e70f841-c261-413b-abb2-2d68cdb96094'
205
+
206
+ export interface SeedAdmin {
207
+ name: string
208
+ login: string
209
+ email: string
210
+ password: string
211
+ }
212
+
213
+ export interface SeedOptions {
214
+ admin: SeedAdmin
215
+ hashPassword: (password: string) => Promise<string>
216
+ passwordConfig: string
217
+ }
218
+
219
+ async function tableIsEmpty(db: Db, table: string): Promise<boolean> {
220
+ const rows = await db.query<{ n: number }>(`SELECT COUNT(*) AS n FROM ${table}`)
221
+ return Number(rows[0]?.n ?? 0) === 0
222
+ }
223
+
224
+ /** Idempotent: does nothing when the identity tables already hold data. */
225
+ export async function seedIdentity(db: Db, options: SeedOptions): Promise<boolean> {
226
+ if (!(await tableIsEmpty(db, 'user_group'))) return false
227
+
228
+ const now = DbDate.toDb(new Date())
229
+ const bool = (value: boolean) => db.dialect.boolValue(value)
230
+
231
+ for (const group of BUILT_IN_GROUPS) {
232
+ await db.exec(
233
+ `INSERT INTO user_group
234
+ (id, key, alias, name, icon, has_access_to_all_languages,
235
+ start_content_id, start_media_id, start_element_id, create_date, update_date)
236
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
237
+ [
238
+ group.id,
239
+ group.key,
240
+ group.alias,
241
+ group.name,
242
+ group.icon,
243
+ bool(group.hasAccessToAllLanguages),
244
+ -1,
245
+ -1,
246
+ -1,
247
+ now,
248
+ now,
249
+ ],
250
+ )
251
+ for (const section of group.sections) {
252
+ await db.exec('INSERT INTO user_group_section (user_group_id, app_alias) VALUES (?, ?)', [
253
+ group.id,
254
+ section,
255
+ ])
256
+ }
257
+ }
258
+ const sync = db.dialect.syncIdentity('user_group')
259
+ if (sync) await db.exec(sync)
260
+ await grantBuiltInPermissions(db)
261
+
262
+ await db.exec(
263
+ `INSERT INTO user_account
264
+ (key, user_name, login, email, password_hash, password_config, security_stamp, language,
265
+ kind, is_locked_out, is_approved, failed_login_attempts, create_date, update_date)
266
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
267
+ [
268
+ SUPER_USER_KEY,
269
+ options.admin.name,
270
+ options.admin.login,
271
+ options.admin.email,
272
+ await options.hashPassword(options.admin.password),
273
+ options.passwordConfig,
274
+ crypto.randomUUID(),
275
+ 'en-US',
276
+ 0,
277
+ bool(false),
278
+ bool(true),
279
+ 0,
280
+ now,
281
+ now,
282
+ ],
283
+ )
284
+
285
+ const created = await db.query<{ id: number }>('SELECT id FROM user_account WHERE login = ?', [
286
+ options.admin.login,
287
+ ])
288
+ const userId = Number(created[0]?.id)
289
+
290
+ // Umbraco puts the super user in Administrators and Sensitive data.
291
+ for (const groupId of [1, 5]) {
292
+ await db.exec('INSERT INTO user_group_member (user_id, user_group_id) VALUES (?, ?)', [
293
+ userId,
294
+ groupId,
295
+ ])
296
+ }
297
+ return true
298
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `server.role`: what each registered node serves, so the cluster can be read
3
+ * back by role — which nodes are answering the public site, which the editor.
4
+ */
5
+ import type { Db } from '../database.ts'
6
+ import type { Migration } from '../migrations.ts'
7
+ import { schemaOps } from '../schema-ops.ts'
8
+ import { STATE_CLIENT_ID_PREFIX } from './client-id-prefix.ts'
9
+
10
+ export const STATE_SERVER_ROLE = '{6f1d4a8e-0021-4c21-9d3a-8b1e5c7a2f21}'
11
+
12
+ export const serverRoleMigration: Migration = {
13
+ from: STATE_CLIENT_ID_PREFIX,
14
+ to: STATE_SERVER_ROLE,
15
+ name: 'AddServerRole',
16
+ kind: 'expand',
17
+ release: '0.4.0',
18
+ async up(db: Db) {
19
+ await schemaOps(db).addColumn('server', 'role', db.dialect.types.varchar(20))
20
+ },
21
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Migration 004 — `since_version`: the version a file says an element goes
3
+ * live in, kept beside `since_state_id` (the deployment that created the row)
4
+ * so "is this pending on my node" is one comparison. docs/09, "since".
5
+ */
6
+ import type { Db } from '../database.ts'
7
+ import type { Migration } from '../migrations.ts'
8
+ import { schemaOps } from '../schema-ops.ts'
9
+ import { STATE_VALUES } from './values.ts'
10
+
11
+ export const STATE_SINCE_VERSION = '{6f1d4a8e-0004-4c21-9d3a-8b1e5c7a2f04}'
12
+
13
+ export const sinceVersionMigration: Migration = {
14
+ from: STATE_VALUES,
15
+ to: STATE_SINCE_VERSION,
16
+ name: 'AddSinceVersion',
17
+ kind: 'expand',
18
+ release: '0.3.0',
19
+ async up(db: Db) {
20
+ const ops = schemaOps(db)
21
+ const type = db.dialect.types.varchar(50)
22
+ await ops.addColumn('content_type', 'since_version', type)
23
+ await ops.addColumn('property_type', 'since_version', type)
24
+ await ops.addColumn('data_type', 'since_version', type)
25
+ },
26
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Migration 030 — a transfer run records the package it applied.
3
+ *
4
+ * A bundle's content arrives inside an npm package, so the run names the
5
+ * package and the version that brought it. That is what tells the Installed
6
+ * view whether a dependency's content has been applied here, and which release
7
+ * of it. Nullable, because a run before this one has no package to name.
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_PACKAGE } from './bundle-package.ts'
13
+
14
+ export const STATE_TRANSFER_RUN_PACKAGE = '{6f1d4a8e-0030-4d2b-8e5c-7a1f9b3d6e30}'
15
+
16
+ export const transferRunPackageMigration: Migration = {
17
+ from: STATE_BUNDLE_PACKAGE,
18
+ to: STATE_TRANSFER_RUN_PACKAGE,
19
+ name: 'AddTransferRunPackage',
20
+ kind: 'expand',
21
+ release: '0.10.0',
22
+ async up(db: Db) {
23
+ const ops = schemaOps(db)
24
+ await ops.addColumn('content_transfer_run', 'package_name', db.dialect.types.varchar(214))
25
+ await ops.addColumn('content_transfer_run', 'package_version', db.dialect.types.varchar(64))
26
+ },
27
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Migration 019 — what a content import did, so reverting it is deterministic.
3
+ *
4
+ * You could try to find an import in the event log afterwards, by kind and
5
+ * timestamp. It would not work: nothing distinguishes one run's `save` events
6
+ * from an editor's concurrent save in the same second, and once a node exists
7
+ * there is no way to recover that the import is what created it. Both facts are
8
+ * needed to put the site back, so they are recorded as the run happens.
9
+ *
10
+ * `content_transfer_change` holds, per node, the event the node was at before
11
+ * the run and the published event it was serving — which is exactly what
12
+ * "restore the previously-live state" means. Shape follows
13
+ * `assistant_changeset`/`assistant_change`: a header with its items.
14
+ */
15
+ import type { Db } from '../database.ts'
16
+ import type { Migration } from '../migrations.ts'
17
+ import { STATE_CHANGE_REPORT } from './change-report.ts'
18
+
19
+ export const STATE_TRANSFER_RUNS = '{6f1d4a8e-0019-4b7d-8e41-6c2a9f3d5e84}'
20
+
21
+ export const transferRunsMigration: Migration = {
22
+ from: STATE_CHANGE_REPORT,
23
+ to: STATE_TRANSFER_RUNS,
24
+ name: 'AddContentTransferRuns',
25
+ kind: 'expand',
26
+ release: '0.3.0',
27
+ async up(db: Db) {
28
+ const t = db.dialect.types
29
+
30
+ await db.exec(
31
+ `CREATE TABLE content_transfer_run (
32
+ id ${t.varchar(64)} NOT NULL,
33
+ bundle_id ${t.varchar(64)} NOT NULL,
34
+ bundle_label ${t.varchar(255)},
35
+ direction ${t.varchar(16)} NOT NULL,
36
+ reverts_run_id ${t.varchar(64)},
37
+ schema_state_id ${t.integer} NOT NULL,
38
+ started_at ${t.timestamp} NOT NULL,
39
+ finished_at ${t.timestamp},
40
+ applied_by ${t.varchar(255)},
41
+ node_count ${t.integer} NOT NULL,
42
+ status ${t.varchar(16)} NOT NULL,
43
+ PRIMARY KEY (id)
44
+ )`,
45
+ )
46
+ // `content runs` lists them newest first.
47
+ await db.exec(
48
+ 'CREATE INDEX ix_content_transfer_run_started ON content_transfer_run (started_at)',
49
+ )
50
+
51
+ await db.exec(
52
+ `CREATE TABLE content_transfer_change (
53
+ id ${t.identity},
54
+ run_id ${t.varchar(64)} NOT NULL,
55
+ node_key ${t.varchar(255)} NOT NULL,
56
+ kind ${t.varchar(16)} NOT NULL,
57
+ action ${t.varchar(16)} NOT NULL,
58
+ before_event_id ${t.integer},
59
+ before_published_event_id ${t.integer},
60
+ event_id ${t.integer}
61
+ )`,
62
+ )
63
+ await db.exec(
64
+ 'CREATE UNIQUE INDEX ux_content_transfer_change_node ON content_transfer_change (run_id, node_key)',
65
+ )
66
+ // Revert asks "has a later run touched this node?", which is a scan by node.
67
+ await db.exec(
68
+ 'CREATE INDEX ix_content_transfer_change_node ON content_transfer_change (node_key)',
69
+ )
70
+ },
71
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Migration 005 — `upgrade_report`: the pre-upgrade check's findings, in a
3
+ * stable shape the backoffice dashboard reads whatever version is live.
4
+ * docs/packaging-and-upgrades.md, "The upgrade dashboard ships in the first release".
5
+ */
6
+ import type { Db } from '../database.ts'
7
+ import type { Migration } from '../migrations.ts'
8
+ import { STATE_SINCE_VERSION } from './since-version.ts'
9
+
10
+ export const STATE_UPGRADE_REPORT = '{6f1d4a8e-0005-4c21-9d3a-8b1e5c7a2f05}'
11
+
12
+ export const upgradeReportMigration: Migration = {
13
+ from: STATE_SINCE_VERSION,
14
+ to: STATE_UPGRADE_REPORT,
15
+ name: 'AddUpgradeReport',
16
+ kind: 'expand',
17
+ release: '0.3.0',
18
+ async up(db: Db) {
19
+ const t = db.dialect.types
20
+ await db.exec(
21
+ `CREATE TABLE upgrade_report (
22
+ id ${t.identity},
23
+ run_id ${t.varchar(64)} NOT NULL,
24
+ kind ${t.varchar(20)} NOT NULL,
25
+ code ${t.varchar(64)} NOT NULL,
26
+ subject_type ${t.varchar(32)} NOT NULL,
27
+ subject_key ${t.varchar(255)} NOT NULL,
28
+ subject_name ${t.varchar(255)},
29
+ property_alias ${t.varchar(255)},
30
+ culture ${t.varchar(14)},
31
+ message ${t.text} NOT NULL,
32
+ link ${t.text},
33
+ status ${t.varchar(20)} NOT NULL,
34
+ first_seen ${t.timestamp} NOT NULL,
35
+ last_seen ${t.timestamp} NOT NULL,
36
+ resolved_at ${t.timestamp}
37
+ )`,
38
+ )
39
+ await db.exec(
40
+ 'CREATE INDEX ix_upgrade_report_subject ON upgrade_report (code, subject_key, property_alias, culture)',
41
+ )
42
+ },
43
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Migration 011 — what users and localisation store beyond identity: a group's
3
+ * languages (`umbracoUserGroup2Language`), per-user key/value data
4
+ * (`umbracoUserData`), single-use tokens for invites and password resets, API
5
+ * users' client credentials, and dictionary items with their translations
6
+ * (`cmsDictionary`, `cmsLanguageText`), snake-cased. It also renames
7
+ * `user_account.is_disabled` to `is_locked_out`, which is what it always meant:
8
+ * a disabled user is one that is not approved, as in Umbraco. And each culture
9
+ * of a document records the publish event and name it was last published with,
10
+ * so publishing one culture leaves the others as they were published.
11
+ */
12
+ import type { Db } from '../database.ts'
13
+ import type { Migration } from '../migrations.ts'
14
+ import { schemaOps } from '../schema-ops.ts'
15
+ import { STATE_CONTENT_EDITING } from './content-editing.ts'
16
+
17
+ export const STATE_USERS_LOCALISATION = '{6f1d4a8e-0011-4c21-9d3a-8b1e5c7a2f11}'
18
+
19
+ export const usersLocalisationMigration: Migration = {
20
+ from: STATE_CONTENT_EDITING,
21
+ to: STATE_USERS_LOCALISATION,
22
+ name: 'AddUserAndLocalisationTables',
23
+ kind: 'expand',
24
+ release: '0.3.0',
25
+ async up(db: Db) {
26
+ const t = db.dialect.types
27
+ // The seed inserted the built-in groups with explicit ids, which left
28
+ // Postgres' identity where the next group would collide with them
29
+ const sync = db.dialect.syncIdentity('user_group')
30
+ if (sync) await db.exec(sync)
31
+ const ops = schemaOps(db)
32
+ await ops.renameColumn('user_account', 'is_disabled', 'is_locked_out')
33
+ await ops.addColumn('document_culture_variation', 'published_event_id', t.integer)
34
+ await ops.addColumn('document_culture_variation', 'published_name', t.varchar(255))
35
+ await db.exec(
36
+ `CREATE TABLE user_group_language (
37
+ user_group_id ${t.integer} NOT NULL REFERENCES user_group (id),
38
+ language_id ${t.integer} NOT NULL REFERENCES language (id),
39
+ PRIMARY KEY (user_group_id, language_id)
40
+ )`,
41
+ )
42
+ await db.exec(
43
+ `CREATE TABLE user_data (
44
+ key ${t.uuid} PRIMARY KEY,
45
+ user_id ${t.integer} NOT NULL REFERENCES user_account (id),
46
+ data_group ${t.varchar(255)} NOT NULL,
47
+ identifier ${t.varchar(255)} NOT NULL,
48
+ value ${t.text} NOT NULL
49
+ )`,
50
+ )
51
+ await db.exec('CREATE UNIQUE INDEX ux_user_data ON user_data (user_id, data_group, identifier)')
52
+ await db.exec(
53
+ `CREATE TABLE user_token (
54
+ token_hash ${t.varchar(128)} PRIMARY KEY,
55
+ user_id ${t.integer} NOT NULL REFERENCES user_account (id),
56
+ purpose ${t.varchar(20)} NOT NULL,
57
+ expires_at ${t.timestamp} NOT NULL
58
+ )`,
59
+ )
60
+ await db.exec(
61
+ `CREATE TABLE user_client_credential (
62
+ client_id ${t.varchar(255)} PRIMARY KEY,
63
+ user_id ${t.integer} NOT NULL REFERENCES user_account (id),
64
+ secret_hash ${t.text} NOT NULL,
65
+ create_date ${t.timestamp} NOT NULL
66
+ )`,
67
+ )
68
+ await db.exec(
69
+ `CREATE TABLE dictionary_item (
70
+ id ${t.identity},
71
+ key ${t.uuid} NOT NULL UNIQUE,
72
+ parent_key ${t.uuid} NULL REFERENCES dictionary_item (key),
73
+ item_key ${t.textCi} NOT NULL UNIQUE
74
+ )`,
75
+ )
76
+ await db.exec(
77
+ `CREATE TABLE dictionary_text (
78
+ dictionary_key ${t.uuid} NOT NULL REFERENCES dictionary_item (key),
79
+ language_id ${t.integer} NOT NULL REFERENCES language (id),
80
+ value ${t.text} NOT NULL,
81
+ PRIMARY KEY (dictionary_key, language_id)
82
+ )`,
83
+ )
84
+ },
85
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Migration 003 — the append-only value layer, schema state, and the columns
3
+ * schema-as-code needs.
4
+ *
5
+ * Pre-1.0, so this replaces `property_data` outright; after 1.0 a change of this
6
+ * shape would be an expand followed by a contract in a later major.
7
+ */
8
+ import type { Db } from '../database.ts'
9
+ import { DbDate } from '../dialect.ts'
10
+ import type { Migration } from '../migrations.ts'
11
+ import { STATE_CONTENT } from './content.ts'
12
+
13
+ export const STATE_VALUES = '{6f1d4a8e-0003-4c21-9d3a-8b1e5c7a2f03}'
14
+
15
+ export const valuesMigration: Migration = {
16
+ from: STATE_CONTENT,
17
+ to: STATE_VALUES,
18
+ name: 'CreateAppendOnlyValues',
19
+ kind: 'expand',
20
+ release: '0.3.0',
21
+ async up(db: Db) {
22
+ const t = db.dialect.types
23
+
24
+ /**
25
+ * One row per sync or upgrade. `current` is what nodes gate writes on;
26
+ * `prepared` is what `upgrade check --fix` creates elements under so they
27
+ * are pending for everyone without blocking anyone.
28
+ */
29
+ await db.exec(
30
+ `CREATE TABLE schema_state (
31
+ id ${t.identity},
32
+ version ${t.varchar(50)} NOT NULL,
33
+ revision ${t.varchar(50)} NOT NULL,
34
+ hash ${t.varchar(64)},
35
+ status ${t.varchar(10)} NOT NULL,
36
+ synced_at ${t.timestamp} NOT NULL,
37
+ synced_by ${t.varchar(255)}
38
+ )`,
39
+ )
40
+ await db.exec(
41
+ `INSERT INTO schema_state (version, revision, hash, status, synced_at, synced_by)
42
+ VALUES (?, ?, ?, ?, ?, ?)`,
43
+ ['0', '0', null, 'current', DbDate.toDb(new Date()), 'install'],
44
+ )
45
+
46
+ /** The event kind: save | publish | rollback | migrate. */
47
+ await db.exec(`ALTER TABLE content_version ADD COLUMN kind ${t.varchar(10)}`)
48
+
49
+ await db.exec(
50
+ `CREATE TABLE property_value (
51
+ id ${t.identity},
52
+ node_id ${t.integer} NOT NULL REFERENCES node (id),
53
+ property_type_id ${t.integer} NOT NULL REFERENCES property_type (id),
54
+ language_id ${t.integer} REFERENCES language (id),
55
+ segment ${t.varchar(256)},
56
+ event_id ${t.integer} NOT NULL REFERENCES content_version (id),
57
+ schema_state_id ${t.integer} NOT NULL REFERENCES schema_state (id),
58
+ is_current ${t.boolean} NOT NULL,
59
+ int_value ${t.integer},
60
+ decimal_value ${t.numeric},
61
+ date_value ${t.timestamp},
62
+ varchar_value ${t.varchar(512)},
63
+ text_value ${t.text},
64
+ sortable_value ${t.varchar(512)}
65
+ )`,
66
+ )
67
+ // The one index that serves the draft read, the as-of reads and the
68
+ // per-key "latest" lookups.
69
+ await db.exec(
70
+ `CREATE INDEX ix_property_value_key ON property_value
71
+ (node_id, property_type_id, language_id, segment, id DESC)`,
72
+ )
73
+ await db.exec('CREATE INDEX ix_property_value_event ON property_value (event_id)')
74
+ await db.exec('CREATE INDEX ix_property_value_current ON property_value (node_id, is_current)')
75
+
76
+ await db.exec('DROP TABLE property_data')
77
+
78
+ // Schema-as-code references data types by alias; Umbraco has only a key and a name.
79
+ await db.exec(`ALTER TABLE data_type ADD COLUMN alias ${t.textCi}`)
80
+
81
+ // Schema-as-code: the deployment that introduced an element, and retirement.
82
+ for (const table of ['content_type', 'property_type', 'data_type']) {
83
+ await db.exec(
84
+ `ALTER TABLE ${table} ADD COLUMN since_state_id ${t.integer} REFERENCES schema_state (id)`,
85
+ )
86
+ await db.exec(`ALTER TABLE ${table} ADD COLUMN retired_at ${t.timestamp}`)
87
+ }
88
+
89
+ // Multi-node plumbing: node identity and the polled change log.
90
+ await db.exec(
91
+ `CREATE TABLE server (
92
+ node_id ${t.varchar(255)} PRIMARY KEY,
93
+ version ${t.varchar(50)},
94
+ revision ${t.varchar(50)},
95
+ last_seen ${t.timestamp} NOT NULL
96
+ )`,
97
+ )
98
+ await db.exec(
99
+ `CREATE TABLE cache_instruction (
100
+ id ${t.identity},
101
+ kind ${t.varchar(50)} NOT NULL,
102
+ payload ${t.text},
103
+ created_at ${t.timestamp} NOT NULL,
104
+ created_by ${t.varchar(255)}
105
+ )`,
106
+ )
107
+ },
108
+ }