@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,74 @@
1
+ /**
2
+ * Migration 017 — changesets: what the assistant has proposed and nobody has
3
+ * approved yet.
4
+ *
5
+ * The assistant never writes to the CMS. It appends to a changeset, and a person
6
+ * approves each item, so the proposal has to outlive the conversation that
7
+ * produced it: a changeset raised over MCP by an agent in a terminal is reviewed
8
+ * in the backoffice by a user in a browser, in another process.
9
+ *
10
+ * `body` holds the Management API request the approval will dispatch, as JSON, so
11
+ * an approved item needs no translation — the proposal *is* a prepared request,
12
+ * and applying it takes the same authorised path as the editor's own Save.
13
+ * `baseline` is what the entity looked like when the proposal was made; apply
14
+ * refuses when it no longer matches, rather than overwriting someone's work.
15
+ *
16
+ * The tables are created whether or not the assistant is configured, and sit
17
+ * empty when it is not. A migration conditional on configuration would make
18
+ * schema state depend on a config file, which the upgrade plan cannot express.
19
+ */
20
+ import type { Db } from '../database.ts'
21
+ import type { Migration } from '../migrations.ts'
22
+ import { STATE_ELEMENTS } from './elements.ts'
23
+
24
+ export const STATE_ASSISTANT = '{6f1d4a8e-0017-4c92-8f3b-2a7e5d1c4b80}'
25
+
26
+ export const assistantMigration: Migration = {
27
+ from: STATE_ELEMENTS,
28
+ to: STATE_ASSISTANT,
29
+ name: 'AddAssistantChangesets',
30
+ kind: 'expand',
31
+ release: '0.3.0',
32
+ async up(db: Db) {
33
+ const t = db.dialect.types
34
+
35
+ await db.exec(
36
+ `CREATE TABLE assistant_changeset (
37
+ id ${t.identity},
38
+ key ${t.text} NOT NULL,
39
+ user_key ${t.text} NOT NULL,
40
+ title ${t.text} NOT NULL,
41
+ origin ${t.varchar(16)} NOT NULL,
42
+ create_date ${t.timestamp} NOT NULL,
43
+ update_date ${t.timestamp} NOT NULL
44
+ )`,
45
+ )
46
+ await db.exec('CREATE UNIQUE INDEX ux_assistant_changeset_key ON assistant_changeset (key)')
47
+ // The drawer lists the signed-in user's own changesets, most recent first.
48
+ await db.exec(
49
+ 'CREATE INDEX ix_assistant_changeset_user ON assistant_changeset (user_key, update_date)',
50
+ )
51
+
52
+ await db.exec(
53
+ `CREATE TABLE assistant_change (
54
+ id ${t.identity},
55
+ key ${t.text} NOT NULL,
56
+ changeset_key ${t.text} NOT NULL,
57
+ sort_order ${t.integer} NOT NULL,
58
+ kind ${t.varchar(32)} NOT NULL,
59
+ summary ${t.text} NOT NULL,
60
+ status ${t.varchar(16)} NOT NULL,
61
+ params ${t.text} NOT NULL,
62
+ body ${t.text} NOT NULL,
63
+ baseline ${t.text} NULL,
64
+ problems ${t.text} NOT NULL,
65
+ error ${t.text} NULL,
66
+ create_date ${t.timestamp} NOT NULL
67
+ )`,
68
+ )
69
+ await db.exec('CREATE UNIQUE INDEX ux_assistant_change_key ON assistant_change (key)')
70
+ await db.exec(
71
+ 'CREATE INDEX ix_assistant_change_changeset ON assistant_change (changeset_key, sort_order)',
72
+ )
73
+ },
74
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Content types this CMS ships. "Search & sharing" (`seoFields`) is a
3
+ * composition a site opts into with `compositions = ["seoFields"]`: the fields
4
+ * the head helper, the sitemap and the editor's preview read.
5
+ *
6
+ * A **system** type, like the Folder, Image and File media types: ensured at
7
+ * every boot, exempt from schema retirement, not deletable, alias fixed. A site
8
+ * may extend it with a file of the same alias and key; the shipped fields stay.
9
+ */
10
+ import {
11
+ type ContentTypeAggregate,
12
+ SEO_FIELDS_PROPERTIES,
13
+ SYSTEM_CONTENT_TYPE_KEYS,
14
+ type SystemPropertyModel,
15
+ } from '@crombolo/core'
16
+ import type { Db } from '../database.ts'
17
+ import { ContentTypeRepository } from '../repositories/content-types.ts'
18
+
19
+ export interface BuiltInContentType {
20
+ key: string
21
+ alias: string
22
+ name: string
23
+ description: string
24
+ icon: string
25
+ tab: { key: string; name: string; alias: string }
26
+ properties: readonly SystemPropertyModel[]
27
+ }
28
+
29
+ export const SEO_FIELDS_TYPE: BuiltInContentType = {
30
+ key: SYSTEM_CONTENT_TYPE_KEYS.SeoFields,
31
+ alias: 'seoFields',
32
+ name: 'Search & sharing',
33
+ description: 'How a page appears in search results and when shared.',
34
+ icon: 'icon-search',
35
+ tab: { key: '3f8a1d6c-5e2b-4a07-9c84-b1e7d0f2a658', name: 'Search & sharing', alias: 'seo' },
36
+ properties: SEO_FIELDS_PROPERTIES,
37
+ }
38
+
39
+ export const SYSTEM_CONTENT_TYPES: readonly BuiltInContentType[] = [SEO_FIELDS_TYPE]
40
+
41
+ export function systemContentTypeAggregate(def: BuiltInContentType): ContentTypeAggregate {
42
+ return {
43
+ key: def.key,
44
+ alias: def.alias,
45
+ name: def.name,
46
+ description: def.description,
47
+ icon: def.icon,
48
+ allowedAsRoot: false,
49
+ variesByCulture: false,
50
+ variesBySegment: false,
51
+ isElement: false,
52
+ allowedInLibrary: false,
53
+ collectionKey: null,
54
+ cleanup: {
55
+ preventCleanup: false,
56
+ keepAllVersionsNewerThanDays: null,
57
+ keepLatestVersionPerDayForDays: null,
58
+ },
59
+ containers: [
60
+ {
61
+ key: def.tab.key,
62
+ name: def.tab.name,
63
+ alias: def.tab.alias,
64
+ type: 'Tab',
65
+ sortOrder: 0,
66
+ parentKey: null,
67
+ },
68
+ ],
69
+ properties: def.properties.map((p, sortOrder) => ({
70
+ key: p.key,
71
+ alias: p.alias,
72
+ name: p.name,
73
+ description: p.description,
74
+ dataTypeKey: p.dataTypeKey,
75
+ containerKey: def.tab.key,
76
+ sortOrder,
77
+ variesByCulture: false,
78
+ variesBySegment: false,
79
+ mandatory: false,
80
+ mandatoryMessage: null,
81
+ regEx: null,
82
+ regExMessage: null,
83
+ labelOnTop: false,
84
+ })),
85
+ compositions: [],
86
+ allowedContentTypes: [],
87
+ allowedComponentKeys: [],
88
+ defaultComponentKey: null,
89
+ parentKey: null,
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Creates any missing system content type by its fixed key; never changes one
95
+ * that exists. Idempotent; runs at every boot.
96
+ */
97
+ export async function ensureSystemContentTypes(db: Db): Promise<string[]> {
98
+ const repo = new ContentTypeRepository(db, { kind: 'document' })
99
+ const created: string[] = []
100
+ for (const def of SYSTEM_CONTENT_TYPES) {
101
+ if (await repo.byKey(def.key)) continue
102
+ await repo.save(systemContentTypeAggregate(def))
103
+ created.push(def.alias)
104
+ }
105
+ return created
106
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Umbraco's seven built-in media types, verbatim from its installer
3
+ * (`DatabaseDataCreator`): keys, aliases, names, icons, the one group each
4
+ * carries, and every property with its key.
5
+ *
6
+ * Folder, Image and File are **system** media types (Umbraco's
7
+ * `IsSystemMediaType`): shipped with the framework, ensured at every boot, not
8
+ * deletable, alias fixed. Video, Audio, Article and Vector Graphics are
9
+ * ordinary types a site owns as files in `schema/media-types/`; `crombolo
10
+ * init` scaffolds them and deleting one deletes its file.
11
+ */
12
+ import { type ContentTypeAggregate, normaliseUuid, SYSTEM_MEDIA_TYPE_KEYS } from '@crombolo/core'
13
+ import type { Db } from '../database.ts'
14
+ import { ContentTypeRepository } from '../repositories/content-types.ts'
15
+ import { DEFAULT_DATA_TYPES } from './content-seed.ts'
16
+
17
+ export interface BuiltInMediaProperty {
18
+ key: string
19
+ alias: string
20
+ name: string
21
+ /** A built-in data type alias. */
22
+ dataType: string
23
+ mandatory: boolean
24
+ }
25
+
26
+ export interface BuiltInMediaType {
27
+ key: string
28
+ alias: string
29
+ name: string
30
+ icon: string
31
+ /** Folder, Image and File: undeletable, alias fixed. */
32
+ system: boolean
33
+ /** A built-in data type alias for the collection view. */
34
+ collection?: string
35
+ /** Aliases; Folder allows every built-in, the ones a site has. */
36
+ allowChildren: string[]
37
+ group?: { key: string; name: string; alias: string }
38
+ properties: BuiltInMediaProperty[]
39
+ }
40
+
41
+ const extension = (key: string): BuiltInMediaProperty => ({
42
+ key,
43
+ alias: 'umbracoExtension',
44
+ name: 'File extension',
45
+ dataType: 'label',
46
+ mandatory: false,
47
+ })
48
+ const bytes = (key: string): BuiltInMediaProperty => ({
49
+ key,
50
+ alias: 'umbracoBytes',
51
+ name: 'File size',
52
+ dataType: 'labelBytes',
53
+ mandatory: false,
54
+ })
55
+ const pixels = (key: string, alias: 'umbracoWidth' | 'umbracoHeight'): BuiltInMediaProperty => ({
56
+ key,
57
+ alias,
58
+ name: alias === 'umbracoWidth' ? 'Width' : 'Height',
59
+ dataType: 'labelPixels',
60
+ mandatory: false,
61
+ })
62
+
63
+ export const BUILT_IN_MEDIA_TYPES: readonly BuiltInMediaType[] = [
64
+ {
65
+ key: SYSTEM_MEDIA_TYPE_KEYS.Folder,
66
+ alias: 'Folder',
67
+ name: 'Folder',
68
+ icon: 'icon-folder',
69
+ system: true,
70
+ collection: 'listViewMedia',
71
+ allowChildren: [
72
+ 'Folder',
73
+ 'Image',
74
+ 'File',
75
+ 'umbracoMediaVideo',
76
+ 'umbracoMediaAudio',
77
+ 'umbracoMediaArticle',
78
+ 'umbracoMediaVectorGraphics',
79
+ ],
80
+ properties: [],
81
+ },
82
+ {
83
+ key: SYSTEM_MEDIA_TYPE_KEYS.Image,
84
+ alias: 'Image',
85
+ name: 'Image',
86
+ icon: 'icon-picture',
87
+ system: true,
88
+ allowChildren: [],
89
+ group: { key: '79ed4d07-254a-42cf-8fa9-ebe1c116a596', name: 'Image', alias: 'image' },
90
+ properties: [
91
+ {
92
+ key: 'b646ca8f-e469-4fc2-a48a-d4dc1aa64a53',
93
+ alias: 'umbracoFile',
94
+ name: 'Image',
95
+ dataType: 'imageCropper',
96
+ mandatory: true,
97
+ },
98
+ pixels('a68d453b-1f62-44f4-9f71-0b6bbd43c355', 'umbracoWidth'),
99
+ pixels('854087f6-648b-40ed-bc98-b8a9789e80b9', 'umbracoHeight'),
100
+ bytes('bd4c5ace-26e3-4a8b-af1a-e8206a35fa07'),
101
+ extension('f7786fe8-724a-4ed0-b244-72546db32a92'),
102
+ ],
103
+ },
104
+ {
105
+ key: SYSTEM_MEDIA_TYPE_KEYS.File,
106
+ alias: 'File',
107
+ name: 'File',
108
+ icon: 'icon-document',
109
+ system: true,
110
+ allowChildren: [],
111
+ group: { key: '50899f9c-023a-4466-b623-aba9049885fe', name: 'File', alias: 'file' },
112
+ properties: [
113
+ {
114
+ key: 'a0fb68f3-f427-47a6-afce-536ffa5b64e9',
115
+ alias: 'umbracoFile',
116
+ name: 'File',
117
+ dataType: 'upload',
118
+ mandatory: true,
119
+ },
120
+ extension('3531c0a3-4e0a-4324-a621-b9d3822b071f'),
121
+ bytes('f9527050-59bc-43e4-8fa8-1658d1319ff5'),
122
+ ],
123
+ },
124
+ {
125
+ key: 'f6c515bb-653c-4bdc-821c-987729ebe327',
126
+ alias: 'umbracoMediaVideo',
127
+ name: 'Video',
128
+ icon: 'icon-video',
129
+ system: false,
130
+ allowChildren: [],
131
+ group: { key: '2f0a61b6-cf92-4ff4-b437-751ab35eb254', name: 'Video', alias: 'video' },
132
+ properties: [
133
+ {
134
+ key: 'bed8ab97-d85f-44d2-a8b9-aef6893f9610',
135
+ alias: 'umbracoFile',
136
+ name: 'Video',
137
+ dataType: 'uploadVideo',
138
+ mandatory: true,
139
+ },
140
+ extension('edd2b3fd-1e57-4e57-935e-096defccdc9b'),
141
+ bytes('180eeecf-1f00-409e-8234-bba967e08b0a'),
142
+ ],
143
+ },
144
+ {
145
+ key: 'a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3',
146
+ alias: 'umbracoMediaAudio',
147
+ name: 'Audio',
148
+ icon: 'icon-audio-lines',
149
+ system: false,
150
+ allowChildren: [],
151
+ group: { key: '335fb495-0a87-4e82-b902-30eb367b767c', name: 'Audio', alias: 'audio' },
152
+ properties: [
153
+ {
154
+ key: '1f48d730-f174-4684-afad-a335e59d84a0',
155
+ alias: 'umbracoFile',
156
+ name: 'Audio',
157
+ dataType: 'uploadAudio',
158
+ mandatory: true,
159
+ },
160
+ extension('1bee433f-a21a-4031-8e03-af01bb8d2de9'),
161
+ bytes('3cbf538a-29ab-4317-a9eb-bbcdf1a54260'),
162
+ ],
163
+ },
164
+ {
165
+ key: 'a43e3414-9599-4230-a7d3-943a21b20122',
166
+ alias: 'umbracoMediaArticle',
167
+ name: 'Article',
168
+ icon: 'icon-article',
169
+ system: false,
170
+ allowChildren: [],
171
+ group: { key: '9af3bd65-f687-4453-9518-5f180d1898ec', name: 'Article', alias: 'article' },
172
+ properties: [
173
+ {
174
+ key: 'e5c8c2d0-2d82-4f01-b53a-45a1d1cbf19c',
175
+ alias: 'umbracoFile',
176
+ name: 'Article',
177
+ dataType: 'uploadArticle',
178
+ mandatory: true,
179
+ },
180
+ extension('ef1b4af7-36de-45eb-8c18-a2de07319227'),
181
+ bytes('aab7d00c-7209-4337-be3f-a4421c8d79a0'),
182
+ ],
183
+ },
184
+ {
185
+ key: 'c4b1efcf-a9d5-41c4-9621-e9d273b52a9c',
186
+ alias: 'umbracoMediaVectorGraphics',
187
+ name: 'Vector Graphics (SVG)',
188
+ icon: 'icon-origami',
189
+ system: false,
190
+ allowChildren: [],
191
+ group: {
192
+ key: 'f199b4d7-9e84-439f-8531-f87d9af37711',
193
+ name: 'Vector Graphics',
194
+ alias: 'vectorGraphics',
195
+ },
196
+ properties: [
197
+ {
198
+ key: 'e2a2bdf2-971b-483e-95a1-4104cc06af26',
199
+ alias: 'umbracoFile',
200
+ name: 'Vector Graphics',
201
+ dataType: 'uploadVectorGraphics',
202
+ mandatory: true,
203
+ },
204
+ extension('0f25a89e-2eb7-49bc-a7b4-759a7e4c69f2'),
205
+ bytes('09a07aff-861d-4769-a2b0-c165ebd43d39'),
206
+ pixels('5bc7e468-c53e-41a6-a522-2723f3b94514', 'umbracoWidth'),
207
+ pixels('9e4c2b59-6bc6-4648-bb71-b0f45ddbc274', 'umbracoHeight'),
208
+ ],
209
+ },
210
+ ]
211
+
212
+ export const SYSTEM_MEDIA_TYPES = BUILT_IN_MEDIA_TYPES.filter((t) => t.system)
213
+
214
+ const dataTypeKey = (alias: string): string => {
215
+ const found = DEFAULT_DATA_TYPES.find((d) => d.alias === alias)
216
+ if (!found) throw new Error(`built-in media types reference unknown data type "${alias}"`)
217
+ return found.key
218
+ }
219
+
220
+ /** The aggregate for a built-in, allowing as children only the types given. */
221
+ export function builtInMediaTypeAggregate(
222
+ def: BuiltInMediaType,
223
+ allowedChildKeys: readonly string[],
224
+ ): ContentTypeAggregate {
225
+ return {
226
+ key: def.key,
227
+ alias: def.alias,
228
+ name: def.name,
229
+ description: null,
230
+ icon: def.icon,
231
+ allowedAsRoot: true,
232
+ variesByCulture: false,
233
+ variesBySegment: false,
234
+ isElement: false,
235
+ allowedInLibrary: false,
236
+ collectionKey: def.collection ? dataTypeKey(def.collection) : null,
237
+ cleanup: {
238
+ preventCleanup: false,
239
+ keepAllVersionsNewerThanDays: null,
240
+ keepLatestVersionPerDayForDays: null,
241
+ },
242
+ containers: def.group
243
+ ? [
244
+ {
245
+ key: def.group.key,
246
+ name: def.group.name,
247
+ alias: def.group.alias,
248
+ type: 'Group',
249
+ sortOrder: 0,
250
+ parentKey: null,
251
+ },
252
+ ]
253
+ : [],
254
+ properties: def.properties.map((p, sortOrder) => ({
255
+ key: p.key,
256
+ alias: p.alias,
257
+ name: p.name,
258
+ description: null,
259
+ dataTypeKey: dataTypeKey(p.dataType),
260
+ containerKey: def.group?.key ?? null,
261
+ sortOrder,
262
+ variesByCulture: false,
263
+ variesBySegment: false,
264
+ mandatory: p.mandatory,
265
+ mandatoryMessage: null,
266
+ regEx: null,
267
+ regExMessage: null,
268
+ labelOnTop: false,
269
+ })),
270
+ compositions: [],
271
+ allowedContentTypes: allowedChildKeys.map((contentTypeKey, sortOrder) => ({
272
+ contentTypeKey,
273
+ sortOrder,
274
+ })),
275
+ allowedComponentKeys: [],
276
+ defaultComponentKey: null,
277
+ parentKey: null,
278
+ }
279
+ }
280
+
281
+ export interface EnsureMediaTypesOptions {
282
+ /**
283
+ * Link Folder to every built-in media type the site has. Skipped when the
284
+ * site defines Folder in a file: then the file says what Folder allows.
285
+ */
286
+ linkFolderChildren?: boolean
287
+ }
288
+
289
+ /**
290
+ * Creates any missing system media type (Folder, Image, File) by its fixed
291
+ * key; never changes one that exists. Idempotent; runs at every boot, before
292
+ * and after the schema sync.
293
+ */
294
+ export async function ensureSystemMediaTypes(
295
+ db: Db,
296
+ options: EnsureMediaTypesOptions = {},
297
+ ): Promise<string[]> {
298
+ const repo = new ContentTypeRepository(db, { kind: 'media' })
299
+ const created: string[] = []
300
+ for (const def of SYSTEM_MEDIA_TYPES) {
301
+ if (await repo.byKey(def.key)) continue
302
+ const systemChildren = def.allowChildren
303
+ .map((alias) => SYSTEM_MEDIA_TYPES.find((s) => s.alias === alias)?.key)
304
+ .filter((key): key is string => key !== undefined)
305
+ await repo.save(builtInMediaTypeAggregate(def, systemChildren))
306
+ created.push(def.alias)
307
+ }
308
+ if (options.linkFolderChildren) {
309
+ const folderDef = BUILT_IN_MEDIA_TYPES.find((t) => t.alias === 'Folder') as BuiltInMediaType
310
+ const folder = await repo.byKey(folderDef.key)
311
+ if (folder) {
312
+ const allowed = new Set(
313
+ folder.allowedContentTypes.map((a) => normaliseUuid(a.contentTypeKey)),
314
+ )
315
+ const missing: string[] = []
316
+ for (const alias of folderDef.allowChildren) {
317
+ const def = BUILT_IN_MEDIA_TYPES.find((t) => t.alias === alias)
318
+ if (!def || allowed.has(def.key)) continue
319
+ if (await repo.byKey(def.key)) missing.push(def.key)
320
+ }
321
+ if (missing.length > 0)
322
+ await repo.save({
323
+ ...folder,
324
+ allowedContentTypes: [
325
+ ...folder.allowedContentTypes,
326
+ ...missing.map((contentTypeKey, i) => ({
327
+ contentTypeKey,
328
+ sortOrder: folder.allowedContentTypes.length + i,
329
+ })),
330
+ ],
331
+ })
332
+ }
333
+ }
334
+ return created
335
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Umbraco's one built-in member type, verbatim from its installer
3
+ * (`DatabaseDataCreator`): alias "Member", icon `icon-user`, and no properties
4
+ * — since Umbraco 18 the standard property stubs are empty and everything a
5
+ * member always has (e-mail, username, approval, lockout) is a column on
6
+ * `member`, not a property.
7
+ *
8
+ * It is a **system** type, like the Folder, Image and File media types: shipped
9
+ * with the framework, ensured at every boot, exempt from schema retirement. A
10
+ * site that wants its own member types adds files under `schema/member-types/`;
11
+ * this one is what makes "create a member" work on a fresh install.
12
+ */
13
+ import { type ContentTypeAggregate, SYSTEM_MEMBER_TYPE_KEY } from '@crombolo/core'
14
+ import type { Db } from '../database.ts'
15
+ import { ContentTypeRepository } from '../repositories/content-types.ts'
16
+
17
+ export const SYSTEM_MEMBER_TYPE = {
18
+ key: SYSTEM_MEMBER_TYPE_KEY,
19
+ alias: 'Member',
20
+ name: 'Member',
21
+ icon: 'icon-user',
22
+ } as const
23
+
24
+ export function systemMemberTypeAggregate(): ContentTypeAggregate {
25
+ return {
26
+ key: SYSTEM_MEMBER_TYPE.key,
27
+ alias: SYSTEM_MEMBER_TYPE.alias,
28
+ name: SYSTEM_MEMBER_TYPE.name,
29
+ description: null,
30
+ icon: SYSTEM_MEMBER_TYPE.icon,
31
+ allowedAsRoot: true,
32
+ variesByCulture: false,
33
+ variesBySegment: false,
34
+ isElement: false,
35
+ allowedInLibrary: false,
36
+ collectionKey: null,
37
+ cleanup: {
38
+ preventCleanup: false,
39
+ keepAllVersionsNewerThanDays: null,
40
+ keepLatestVersionPerDayForDays: null,
41
+ },
42
+ containers: [],
43
+ properties: [],
44
+ compositions: [],
45
+ allowedContentTypes: [],
46
+ allowedComponentKeys: [],
47
+ defaultComponentKey: null,
48
+ parentKey: null,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Creates the "Member" member type if it is missing, by its fixed key; never
54
+ * changes one that exists. Idempotent, and run at every boot.
55
+ */
56
+ export async function ensureSystemMemberTypes(db: Db): Promise<string[]> {
57
+ const repo = new ContentTypeRepository(db, { kind: 'member' })
58
+ if (await repo.byKey(SYSTEM_MEMBER_TYPE.key)) return []
59
+ await repo.save(systemMemberTypeAggregate())
60
+ return [SYSTEM_MEMBER_TYPE.alias]
61
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Migration 028 — a bundle's `templates` and `partial_views` become `components`.
3
+ *
4
+ * There is one root and one kind of file on disk now (`docs/rendering.md`),
5
+ * so a bundle definition that still kept two lists was recording a distinction
6
+ * the rest of the system no longer makes. The two are merged rather than one
7
+ * dropped: both held component aliases, and which tree the backoffice picked
8
+ * one from is not worth keeping.
9
+ *
10
+ * A new migration rather than an edit to 022, for the reason 027 gives: 022 has
11
+ * already run on every database that exists.
12
+ */
13
+ import type { Db } from '../database.ts'
14
+ import type { Migration } from '../migrations.ts'
15
+ import { schemaOps } from '../schema-ops.ts'
16
+ import { STATE_BUNDLES } from './bundles-rename.ts'
17
+
18
+ export const STATE_BUNDLE_COMPONENTS = '{6f1d4a8e-0028-4a19-8c64-2f5b9d3e7a14}'
19
+
20
+ /** A column holding no aliases, however it was written. */
21
+ const EMPTY = (column: string) => `(${column} IS NULL OR ${column} IN ('', '[]'))`
22
+
23
+ export const bundleComponentsMigration: Migration = {
24
+ from: STATE_BUNDLES,
25
+ to: STATE_BUNDLE_COMPONENTS,
26
+ name: 'MergeBundleTemplatesAndPartialViewsIntoComponents',
27
+ kind: 'expand',
28
+ release: '0.6.0',
29
+ async up(db: Db) {
30
+ const ops = schemaOps(db)
31
+ // Renamed rather than added, so the existing values carry over without a
32
+ // default and without a NOT NULL that an empty table could not satisfy.
33
+ await ops.renameColumn('bundle', 'templates', 'components')
34
+ // Both are JSON arrays of aliases, so the merge is `[a,b]` + `[c]` done as
35
+ // text: drop the closing bracket from one, the opening from the other.
36
+ // `||` is the concatenation operator in both dialects.
37
+ await db.exec(
38
+ `UPDATE bundle SET components = partial_views
39
+ WHERE ${EMPTY('components')} AND NOT ${EMPTY('partial_views')}`,
40
+ )
41
+ await db.exec(
42
+ `UPDATE bundle
43
+ SET components = SUBSTR(components, 1, LENGTH(components) - 1) || ',' ||
44
+ SUBSTR(partial_views, 2)
45
+ WHERE NOT ${EMPTY('components')} AND NOT ${EMPTY('partial_views')}`,
46
+ )
47
+ await ops.dropColumn('bundle', 'partial_views')
48
+ },
49
+ }
@@ -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
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Migration 027 — `created_package` becomes `bundle`.
3
+ *
4
+ * The table was named after Umbraco's `umbracoCreatedPackageSchema`, which it
5
+ * mirrors column for column. What it holds is a bundle definition
6
+ * (`docs/bundles.md`), and the backoffice now says so, so the table does too.
7
+ *
8
+ * A rename rather than an edit to migration 022: 022 has already run on every
9
+ * database that exists, and rewriting it would leave those sites with a table
10
+ * the code no longer looks for and no migration to tell them so.
11
+ */
12
+ import type { Db } from '../database.ts'
13
+ import type { Migration } from '../migrations.ts'
14
+ import { STATE_FORM_PERMISSION_PREFIX } from './form-permission-prefix.ts'
15
+
16
+ export const STATE_BUNDLES = '{6f1d4a8e-0027-4e72-9a55-3d7c1b8e4f02}'
17
+
18
+ export const bundlesRenameMigration: Migration = {
19
+ from: STATE_FORM_PERMISSION_PREFIX,
20
+ to: STATE_BUNDLES,
21
+ name: 'RenameCreatedPackageToBundle',
22
+ kind: 'expand',
23
+ release: '0.5.0',
24
+ async up(db: Db) {
25
+ await db.exec('ALTER TABLE created_package RENAME TO bundle')
26
+ // Neither dialect carries an index name through a table rename, so the
27
+ // unique name constraint is dropped and rebuilt rather than left reading
28
+ // `ux_created_package_name` on a table that no longer exists.
29
+ await db.exec('DROP INDEX ux_created_package_name')
30
+ await db.exec('CREATE UNIQUE INDEX ux_bundle_name ON bundle (name)')
31
+ },
32
+ }