@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.
- package/LICENSE +21 -0
- package/NOTICE +69 -0
- package/package.json +31 -4
- 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/change-report.ts +166 -0
- package/src/cluster.ts +108 -0
- package/src/database.ts +121 -0
- package/src/dialect.ts +136 -0
- package/src/index.ts +63 -0
- package/src/locks.ts +103 -0
- package/src/maintenance.ts +51 -0
- package/src/migrations.ts +547 -0
- package/src/node-ref.ts +107 -0
- package/src/pending.ts +25 -0
- package/src/repositories/bundles.ts +223 -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 +208 -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/bundle-package.ts +27 -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 +77 -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-run-package.ts +27 -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,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
|
+
}
|