@crombolo/data 0.0.0-stage → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +69 -0
- package/package.json +31 -4
- package/src/change-report.ts +166 -0
- package/src/cluster.ts +108 -0
- package/src/database.ts +121 -0
- package/src/dialect.ts +128 -0
- package/src/index.ts +60 -0
- package/src/locks.ts +103 -0
- package/src/maintenance.ts +51 -0
- package/src/migrations.ts +431 -0
- package/src/node-ref.ts +107 -0
- package/src/pending.ts +25 -0
- package/src/repositories/bundles.ts +191 -0
- package/src/repositories/content-types.ts +831 -0
- package/src/repositories/data-types.ts +222 -0
- package/src/repositories/dictionary.ts +259 -0
- package/src/repositories/documents.ts +2389 -0
- package/src/repositories/domains.ts +108 -0
- package/src/repositories/folders.ts +120 -0
- package/src/repositories/form-entries.ts +314 -0
- package/src/repositories/form-workflows.ts +213 -0
- package/src/repositories/languages.ts +108 -0
- package/src/repositories/log-searches.ts +33 -0
- package/src/repositories/members.ts +427 -0
- package/src/repositories/nodes.ts +361 -0
- package/src/repositories/notifications.ts +54 -0
- package/src/repositories/public-access.ts +187 -0
- package/src/repositories/redirects.ts +274 -0
- package/src/repositories/references.ts +162 -0
- package/src/repositories/tags.ts +93 -0
- package/src/repositories/templates.ts +114 -0
- package/src/repositories/transfer-runs.ts +190 -0
- package/src/repositories/user-data.ts +90 -0
- package/src/repositories/users.ts +898 -0
- package/src/schema/assistant.ts +74 -0
- package/src/schema/built-in-content-types.ts +106 -0
- package/src/schema/built-in-media-types.ts +335 -0
- package/src/schema/built-in-member-types.ts +61 -0
- package/src/schema/bundle-components.ts +49 -0
- package/src/schema/bundles-rename.ts +32 -0
- package/src/schema/change-report.ts +46 -0
- package/src/schema/client-id-prefix.ts +43 -0
- package/src/schema/content-editing.ts +50 -0
- package/src/schema/content-seed.ts +586 -0
- package/src/schema/content.ts +263 -0
- package/src/schema/created-packages.ts +56 -0
- package/src/schema/elements.ts +62 -0
- package/src/schema/form-permission-prefix.ts +50 -0
- package/src/schema/form-permissions.ts +26 -0
- package/src/schema/form-workflows.ts +54 -0
- package/src/schema/forms.ts +63 -0
- package/src/schema/group-permissions.ts +20 -0
- package/src/schema/identity.ts +176 -0
- package/src/schema/log-viewer.ts +68 -0
- package/src/schema/member-property-type.ts +30 -0
- package/src/schema/members.ts +97 -0
- package/src/schema/plan.ts +73 -0
- package/src/schema/public-access.ts +56 -0
- package/src/schema/recycle-bin.ts +21 -0
- package/src/schema/redirects.ts +57 -0
- package/src/schema/rich-text-ui.ts +34 -0
- package/src/schema/seed.ts +298 -0
- package/src/schema/server-role.ts +21 -0
- package/src/schema/since-version.ts +26 -0
- package/src/schema/transfer-runs.ts +71 -0
- package/src/schema/upgrade-report.ts +43 -0
- package/src/schema/users-localisation.ts +85 -0
- package/src/schema/values.ts +108 -0
- package/src/schema-ops.ts +35 -0
- package/src/schema-state.ts +196 -0
- package/README.md +0 -3
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 002 — the content schema.
|
|
3
|
+
*
|
|
4
|
+
* `node` is the universal polymorphic tree; every other table here is a facet of
|
|
5
|
+
* it. See docs/02-data-model.md for the model and why it is kept.
|
|
6
|
+
*/
|
|
7
|
+
import type { Db } from '../database.ts'
|
|
8
|
+
import type { Migration } from '../migrations.ts'
|
|
9
|
+
import { STATE_IDENTITY } from './identity.ts'
|
|
10
|
+
|
|
11
|
+
export const STATE_CONTENT = '{6f1d4a8e-0002-4c21-9d3a-8b1e5c7a2f02}'
|
|
12
|
+
|
|
13
|
+
export const contentMigration: Migration = {
|
|
14
|
+
from: STATE_IDENTITY,
|
|
15
|
+
to: STATE_CONTENT,
|
|
16
|
+
name: 'CreateContentSchema',
|
|
17
|
+
kind: 'expand',
|
|
18
|
+
release: '0.3.0',
|
|
19
|
+
async up(db: Db) {
|
|
20
|
+
const t = db.dialect.types
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The tree. `path` is a materialised ancestor list ("-1,1060,1075") so a
|
|
24
|
+
* descendant query is a prefix match rather than a recursive walk.
|
|
25
|
+
*/
|
|
26
|
+
await db.exec(
|
|
27
|
+
`CREATE TABLE node (
|
|
28
|
+
id ${t.identity},
|
|
29
|
+
unique_id ${t.uuid} NOT NULL UNIQUE,
|
|
30
|
+
parent_id ${t.integer} NOT NULL,
|
|
31
|
+
level ${t.integer} NOT NULL,
|
|
32
|
+
path ${t.varchar(150)} NOT NULL,
|
|
33
|
+
sort_order ${t.integer} NOT NULL,
|
|
34
|
+
trashed ${t.boolean} NOT NULL,
|
|
35
|
+
node_user ${t.integer},
|
|
36
|
+
text ${t.textCi},
|
|
37
|
+
node_object_type ${t.uuid},
|
|
38
|
+
create_date ${t.timestamp} NOT NULL
|
|
39
|
+
)`,
|
|
40
|
+
)
|
|
41
|
+
await db.exec('CREATE INDEX ix_node_parent_object ON node (parent_id, node_object_type)')
|
|
42
|
+
await db.exec(
|
|
43
|
+
'CREATE INDEX ix_node_object_trashed_sort ON node (node_object_type, trashed, sort_order, id)',
|
|
44
|
+
)
|
|
45
|
+
await db.exec('CREATE INDEX ix_node_path ON node (path)')
|
|
46
|
+
await db.exec('CREATE INDEX ix_node_trashed ON node (trashed)')
|
|
47
|
+
|
|
48
|
+
await db.exec(
|
|
49
|
+
`CREATE TABLE language (
|
|
50
|
+
id ${t.identity},
|
|
51
|
+
iso_code ${t.varchar(14)} NOT NULL UNIQUE,
|
|
52
|
+
culture_name ${t.varchar(100)} NOT NULL,
|
|
53
|
+
is_default ${t.boolean} NOT NULL,
|
|
54
|
+
is_mandatory ${t.boolean} NOT NULL,
|
|
55
|
+
fallback_language_id ${t.integer}
|
|
56
|
+
)`,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
await db.exec(
|
|
60
|
+
`CREATE TABLE data_type (
|
|
61
|
+
node_id ${t.integer} PRIMARY KEY REFERENCES node (id),
|
|
62
|
+
editor_alias ${t.varchar(255)} NOT NULL,
|
|
63
|
+
editor_ui_alias ${t.varchar(255)},
|
|
64
|
+
db_type ${t.varchar(50)} NOT NULL,
|
|
65
|
+
config ${t.text}
|
|
66
|
+
)`,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The view file lives on disk at components/{alias}.tsx; the layout chain is
|
|
71
|
+
* declared in the file itself, exactly as Umbraco parses it out of Razor.
|
|
72
|
+
*/
|
|
73
|
+
await db.exec(
|
|
74
|
+
`CREATE TABLE template (
|
|
75
|
+
node_id ${t.integer} PRIMARY KEY REFERENCES node (id),
|
|
76
|
+
alias ${t.varchar(100)} NOT NULL
|
|
77
|
+
)`,
|
|
78
|
+
)
|
|
79
|
+
await db.exec('CREATE UNIQUE INDEX ux_template_alias ON template (alias)')
|
|
80
|
+
|
|
81
|
+
await db.exec(
|
|
82
|
+
`CREATE TABLE content_type (
|
|
83
|
+
node_id ${t.integer} PRIMARY KEY REFERENCES node (id),
|
|
84
|
+
alias ${t.textCi} NOT NULL,
|
|
85
|
+
icon ${t.varchar(255)},
|
|
86
|
+
thumbnail ${t.varchar(255)},
|
|
87
|
+
description ${t.varchar(1500)},
|
|
88
|
+
list_view ${t.uuid},
|
|
89
|
+
is_element ${t.boolean} NOT NULL,
|
|
90
|
+
allow_in_library ${t.boolean} NOT NULL,
|
|
91
|
+
allow_at_root ${t.boolean} NOT NULL,
|
|
92
|
+
variations ${t.integer} NOT NULL,
|
|
93
|
+
prevent_cleanup ${t.boolean} NOT NULL,
|
|
94
|
+
keep_all_versions_newer_than_days ${t.integer},
|
|
95
|
+
keep_latest_version_per_day_for_days ${t.integer}
|
|
96
|
+
)`,
|
|
97
|
+
)
|
|
98
|
+
await db.exec('CREATE UNIQUE INDEX ux_content_type_alias ON content_type (alias)')
|
|
99
|
+
|
|
100
|
+
await db.exec(
|
|
101
|
+
`CREATE TABLE property_type_group (
|
|
102
|
+
id ${t.identity},
|
|
103
|
+
unique_id ${t.uuid} NOT NULL UNIQUE,
|
|
104
|
+
content_type_node_id ${t.integer} NOT NULL REFERENCES content_type (node_id),
|
|
105
|
+
type ${t.integer} NOT NULL,
|
|
106
|
+
text ${t.varchar(255)},
|
|
107
|
+
alias ${t.varchar(255)},
|
|
108
|
+
sort_order ${t.integer} NOT NULL,
|
|
109
|
+
parent_id ${t.integer}
|
|
110
|
+
)`,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
await db.exec(
|
|
114
|
+
`CREATE TABLE property_type (
|
|
115
|
+
id ${t.identity},
|
|
116
|
+
unique_id ${t.uuid} NOT NULL UNIQUE,
|
|
117
|
+
data_type_id ${t.integer} NOT NULL REFERENCES data_type (node_id),
|
|
118
|
+
content_type_id ${t.integer} NOT NULL REFERENCES content_type (node_id),
|
|
119
|
+
property_type_group_id ${t.integer} REFERENCES property_type_group (id),
|
|
120
|
+
alias ${t.textCi} NOT NULL,
|
|
121
|
+
name ${t.varchar(255)} NOT NULL,
|
|
122
|
+
description ${t.varchar(2000)},
|
|
123
|
+
sort_order ${t.integer} NOT NULL,
|
|
124
|
+
mandatory ${t.boolean} NOT NULL,
|
|
125
|
+
mandatory_message ${t.varchar(500)},
|
|
126
|
+
validation_reg_exp ${t.text},
|
|
127
|
+
validation_reg_exp_message ${t.varchar(500)},
|
|
128
|
+
label_on_top ${t.boolean} NOT NULL,
|
|
129
|
+
variations ${t.integer} NOT NULL
|
|
130
|
+
)`,
|
|
131
|
+
)
|
|
132
|
+
await db.exec(
|
|
133
|
+
'CREATE UNIQUE INDEX ux_property_type_alias ON property_type (content_type_id, alias)',
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The composition graph, which is NOT the folder tree: `node.parent_id`
|
|
138
|
+
* places a type in a folder, while this table says which types it inherits
|
|
139
|
+
* properties from.
|
|
140
|
+
*/
|
|
141
|
+
await db.exec(
|
|
142
|
+
`CREATE TABLE content_type_composition (
|
|
143
|
+
parent_content_type_id ${t.integer} NOT NULL REFERENCES node (id),
|
|
144
|
+
child_content_type_id ${t.integer} NOT NULL REFERENCES node (id),
|
|
145
|
+
PRIMARY KEY (parent_content_type_id, child_content_type_id)
|
|
146
|
+
)`,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
await db.exec(
|
|
150
|
+
`CREATE TABLE content_type_allowed_child (
|
|
151
|
+
content_type_id ${t.integer} NOT NULL REFERENCES content_type (node_id),
|
|
152
|
+
allowed_content_type_id ${t.integer} NOT NULL REFERENCES content_type (node_id),
|
|
153
|
+
sort_order ${t.integer} NOT NULL,
|
|
154
|
+
PRIMARY KEY (content_type_id, allowed_content_type_id)
|
|
155
|
+
)`,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
await db.exec(
|
|
159
|
+
`CREATE TABLE content_type_template (
|
|
160
|
+
content_type_node_id ${t.integer} NOT NULL REFERENCES content_type (node_id),
|
|
161
|
+
template_node_id ${t.integer} NOT NULL REFERENCES template (node_id),
|
|
162
|
+
is_default ${t.boolean} NOT NULL,
|
|
163
|
+
PRIMARY KEY (content_type_node_id, template_node_id)
|
|
164
|
+
)`,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// ---- documents and versions (Phase 4 uses these) ----
|
|
168
|
+
|
|
169
|
+
await db.exec(
|
|
170
|
+
`CREATE TABLE content (
|
|
171
|
+
node_id ${t.integer} PRIMARY KEY REFERENCES node (id),
|
|
172
|
+
content_type_id ${t.integer} NOT NULL REFERENCES content_type (node_id)
|
|
173
|
+
)`,
|
|
174
|
+
)
|
|
175
|
+
await db.exec('CREATE INDEX ix_content_content_type ON content (content_type_id)')
|
|
176
|
+
|
|
177
|
+
/** `current = true` marks the draft; exactly one row per node. */
|
|
178
|
+
await db.exec(
|
|
179
|
+
`CREATE TABLE content_version (
|
|
180
|
+
id ${t.identity},
|
|
181
|
+
node_id ${t.integer} NOT NULL REFERENCES content (node_id),
|
|
182
|
+
version_date ${t.timestamp} NOT NULL,
|
|
183
|
+
user_id ${t.integer},
|
|
184
|
+
current ${t.boolean} NOT NULL,
|
|
185
|
+
text ${t.textCi},
|
|
186
|
+
prevent_cleanup ${t.boolean} NOT NULL
|
|
187
|
+
)`,
|
|
188
|
+
)
|
|
189
|
+
await db.exec(
|
|
190
|
+
'CREATE INDEX ix_content_version_node_current ON content_version (node_id, current)',
|
|
191
|
+
)
|
|
192
|
+
await db.exec('CREATE INDEX ix_content_version_date ON content_version (version_date)')
|
|
193
|
+
|
|
194
|
+
await db.exec(
|
|
195
|
+
`CREATE TABLE document (
|
|
196
|
+
node_id ${t.integer} PRIMARY KEY REFERENCES content (node_id),
|
|
197
|
+
published ${t.boolean} NOT NULL,
|
|
198
|
+
edited ${t.boolean} NOT NULL
|
|
199
|
+
)`,
|
|
200
|
+
)
|
|
201
|
+
await db.exec('CREATE INDEX ix_document_published ON document (published)')
|
|
202
|
+
|
|
203
|
+
/** `published = true` marks the published version; at most one per document. */
|
|
204
|
+
await db.exec(
|
|
205
|
+
`CREATE TABLE document_version (
|
|
206
|
+
id ${t.integer} PRIMARY KEY REFERENCES content_version (id),
|
|
207
|
+
template_id ${t.integer} REFERENCES template (node_id),
|
|
208
|
+
published ${t.boolean} NOT NULL
|
|
209
|
+
)`,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Property values. Five typed columns selected by `data_type.db_type`; the
|
|
214
|
+
* unique key is the value's identity.
|
|
215
|
+
*/
|
|
216
|
+
await db.exec(
|
|
217
|
+
`CREATE TABLE property_data (
|
|
218
|
+
id ${t.identity},
|
|
219
|
+
version_id ${t.integer} NOT NULL REFERENCES content_version (id),
|
|
220
|
+
property_type_id ${t.integer} NOT NULL REFERENCES property_type (id),
|
|
221
|
+
language_id ${t.integer} REFERENCES language (id),
|
|
222
|
+
segment ${t.varchar(256)},
|
|
223
|
+
int_value ${t.integer},
|
|
224
|
+
decimal_value ${t.numeric},
|
|
225
|
+
date_value ${t.timestamp},
|
|
226
|
+
varchar_value ${t.varchar(512)},
|
|
227
|
+
text_value ${t.text},
|
|
228
|
+
sortable_value ${t.varchar(512)}
|
|
229
|
+
)`,
|
|
230
|
+
)
|
|
231
|
+
// A NULL language or segment means invariant/neutral, and NULLs do not
|
|
232
|
+
// compare equal, so uniqueness is enforced over coalesced sentinels.
|
|
233
|
+
await db.exec(
|
|
234
|
+
`CREATE UNIQUE INDEX ux_property_data ON property_data
|
|
235
|
+
(version_id, property_type_id, COALESCE(language_id, -1), COALESCE(segment, ''))`,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
await db.exec(
|
|
239
|
+
`CREATE TABLE content_version_culture_variation (
|
|
240
|
+
id ${t.identity},
|
|
241
|
+
version_id ${t.integer} NOT NULL REFERENCES content_version (id),
|
|
242
|
+
language_id ${t.integer} NOT NULL REFERENCES language (id),
|
|
243
|
+
name ${t.textCi} NOT NULL,
|
|
244
|
+
date ${t.timestamp} NOT NULL,
|
|
245
|
+
available_user_id ${t.integer},
|
|
246
|
+
UNIQUE (version_id, language_id)
|
|
247
|
+
)`,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
await db.exec(
|
|
251
|
+
`CREATE TABLE document_culture_variation (
|
|
252
|
+
id ${t.identity},
|
|
253
|
+
node_id ${t.integer} NOT NULL REFERENCES node (id),
|
|
254
|
+
language_id ${t.integer} NOT NULL REFERENCES language (id),
|
|
255
|
+
edited ${t.boolean} NOT NULL,
|
|
256
|
+
available ${t.boolean} NOT NULL,
|
|
257
|
+
published ${t.boolean} NOT NULL,
|
|
258
|
+
name ${t.textCi},
|
|
259
|
+
UNIQUE (node_id, language_id)
|
|
260
|
+
)`,
|
|
261
|
+
)
|
|
262
|
+
},
|
|
263
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 022 — the created-package definitions the Packages section builds.
|
|
3
|
+
*
|
|
4
|
+
* A definition, never an artifact: the zip is built when it is downloaded
|
|
5
|
+
* (`docs/17-bundles.md`), so there are no bytes here to go stale when a
|
|
6
|
+
* content type the package carries is edited afterwards.
|
|
7
|
+
*
|
|
8
|
+
* The selection lists are JSON text rather than a row each. They are read and
|
|
9
|
+
* written whole and never queried into, so a child table would be a join for
|
|
10
|
+
* nothing — and the contract hands them over as arrays in exactly this shape.
|
|
11
|
+
*/
|
|
12
|
+
import type { Db } from '../database.ts'
|
|
13
|
+
import type { Migration } from '../migrations.ts'
|
|
14
|
+
import { STATE_SERVER_ROLE } from './server-role.ts'
|
|
15
|
+
|
|
16
|
+
export const STATE_CREATED_PACKAGES = '{6f1d4a8e-0022-4e72-9a55-3d7c1b8e4f02}'
|
|
17
|
+
|
|
18
|
+
export const createdPackagesMigration: Migration = {
|
|
19
|
+
from: STATE_SERVER_ROLE,
|
|
20
|
+
to: STATE_CREATED_PACKAGES,
|
|
21
|
+
name: 'AddCreatedPackages',
|
|
22
|
+
kind: 'expand',
|
|
23
|
+
release: '0.5.0',
|
|
24
|
+
async up(db: Db) {
|
|
25
|
+
const t = db.dialect.types
|
|
26
|
+
|
|
27
|
+
await db.exec(
|
|
28
|
+
`CREATE TABLE created_package (
|
|
29
|
+
id ${t.varchar(64)} NOT NULL,
|
|
30
|
+
name ${t.textCi} NOT NULL,
|
|
31
|
+
content_node_id ${t.varchar(64)},
|
|
32
|
+
content_load_child_nodes ${t.boolean} NOT NULL,
|
|
33
|
+
media_load_child_nodes ${t.boolean} NOT NULL,
|
|
34
|
+
media_ids ${t.text} NOT NULL,
|
|
35
|
+
element_ids ${t.text},
|
|
36
|
+
content_types ${t.text} NOT NULL,
|
|
37
|
+
media_types ${t.text} NOT NULL,
|
|
38
|
+
data_types ${t.text} NOT NULL,
|
|
39
|
+
templates ${t.text} NOT NULL,
|
|
40
|
+
partial_views ${t.text} NOT NULL,
|
|
41
|
+
stylesheets ${t.text} NOT NULL,
|
|
42
|
+
scripts ${t.text} NOT NULL,
|
|
43
|
+
languages ${t.text} NOT NULL,
|
|
44
|
+
dictionary_items ${t.text} NOT NULL,
|
|
45
|
+
create_date ${t.timestamp} NOT NULL,
|
|
46
|
+
update_date ${t.timestamp} NOT NULL,
|
|
47
|
+
PRIMARY KEY (id)
|
|
48
|
+
)`,
|
|
49
|
+
)
|
|
50
|
+
// Umbraco refuses two packages with the same name, and the list is ordered
|
|
51
|
+
// by it. `textCi` rather than `varchar` so the constraint means the same
|
|
52
|
+
// thing on both dialects: Postgres collates case-sensitively by default,
|
|
53
|
+
// where SQLite's varchar is already NOCASE.
|
|
54
|
+
await db.exec('CREATE UNIQUE INDEX ux_created_package_name ON created_package (name)')
|
|
55
|
+
},
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 016 — the Elements recycle bin node.
|
|
3
|
+
*
|
|
4
|
+
* Elements are Umbraco 18's publishable content without a URL, and they need
|
|
5
|
+
* nothing new in the schema: an element is a node facet exactly as a document is
|
|
6
|
+
* (`node` + `content` + `content_version`), its folders are `node` rows with the
|
|
7
|
+
* ElementContainer object type, and the document repository takes it as a fifth
|
|
8
|
+
* `kind`. The one thing missing is somewhere to trash them into.
|
|
9
|
+
*
|
|
10
|
+
* `seedContent` creates all three bins on a fresh install; this adds the element
|
|
11
|
+
* one to a database seeded before elements existed. Umbraco's own id and object
|
|
12
|
+
* type are used (`Constants.System.RecycleBinElement = -22`), so a database here
|
|
13
|
+
* numbers its bins as Umbraco's does.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ObjectTypes, SystemNodes } from '@crombolo/core'
|
|
17
|
+
import type { Db } from '../database.ts'
|
|
18
|
+
import { DbDate } from '../dialect.ts'
|
|
19
|
+
import type { Migration } from '../migrations.ts'
|
|
20
|
+
import { STATE_REDIRECTS } from './redirects.ts'
|
|
21
|
+
|
|
22
|
+
export const STATE_ELEMENTS = '{6f1d4a8e-0016-4d8a-b1f7-5c3e9a204d61}'
|
|
23
|
+
|
|
24
|
+
export const elementsMigration: Migration = {
|
|
25
|
+
from: STATE_REDIRECTS,
|
|
26
|
+
to: STATE_ELEMENTS,
|
|
27
|
+
name: 'AddElementRecycleBin',
|
|
28
|
+
kind: 'expand',
|
|
29
|
+
release: '0.3.0',
|
|
30
|
+
async up(db: Db) {
|
|
31
|
+
// One statement, with both guards in SQL, because `crombolo upgrade --plan`
|
|
32
|
+
// runs every migration against a recorder whose `query` refuses to read —
|
|
33
|
+
// a migration that reads cannot be planned, and the plan is the thing that
|
|
34
|
+
// lets a release be reviewed before it runs.
|
|
35
|
+
//
|
|
36
|
+
// `FROM node` is the first guard: a fresh database is seeded *after*
|
|
37
|
+
// migrations, and `seedContent` only seeds while `node` is empty, so
|
|
38
|
+
// inserting here would suppress the entire seed — data types included — and
|
|
39
|
+
// leave every content type unresolvable. No rows, no insert; the seed creates
|
|
40
|
+
// all three bins itself. `NOT EXISTS` is the second: idempotent for a
|
|
41
|
+
// database that already has the bin.
|
|
42
|
+
await db.exec(
|
|
43
|
+
`INSERT INTO node
|
|
44
|
+
(id, unique_id, parent_id, level, path, sort_order, trashed, text, node_object_type, create_date)
|
|
45
|
+
SELECT ?, ?, ?, 0, ?, 0, ?, ?, ?, ?
|
|
46
|
+
FROM node
|
|
47
|
+
WHERE NOT EXISTS (SELECT 1 FROM node WHERE id = ?)
|
|
48
|
+
LIMIT 1`,
|
|
49
|
+
[
|
|
50
|
+
SystemNodes.ElementRecycleBin,
|
|
51
|
+
crypto.randomUUID(),
|
|
52
|
+
SystemNodes.Root,
|
|
53
|
+
`${SystemNodes.Root},${SystemNodes.ElementRecycleBin}`,
|
|
54
|
+
db.dialect.boolValue(false),
|
|
55
|
+
'Recycle Bin',
|
|
56
|
+
ObjectTypes.ElementRecycleBin,
|
|
57
|
+
DbDate.toDb(new Date()),
|
|
58
|
+
SystemNodes.ElementRecycleBin,
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
},
|
|
62
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 026 — the form verbs move from `Umb.` to `Crombolo.`.
|
|
3
|
+
*
|
|
4
|
+
* Migration 025 granted them under `Umb.Form.*` and `Umb.FormEntry.*`, which
|
|
5
|
+
* claimed they were Umbraco's. They are not: Umbraco has no forms in core, so
|
|
6
|
+
* nothing upstream defines them and the prefix was a lie a reader could not
|
|
7
|
+
* check. Every database seeded or upgraded since has the old spelling stored.
|
|
8
|
+
*
|
|
9
|
+
* A rewrite rather than a re-grant, so a verb an administrator removed by hand
|
|
10
|
+
* stays removed. `grantBuiltInPermissions` would have put it back.
|
|
11
|
+
*/
|
|
12
|
+
import type { Db } from '../database.ts'
|
|
13
|
+
import type { Migration } from '../migrations.ts'
|
|
14
|
+
import { STATE_FORM_PERMISSIONS } from './form-permissions.ts'
|
|
15
|
+
|
|
16
|
+
export const STATE_FORM_PERMISSION_PREFIX = '{6f1d4a8e-0026-4e72-9a55-3d7c1b8e4f02}'
|
|
17
|
+
|
|
18
|
+
/** Old spelling to new, for every verb migration 025 could have written. */
|
|
19
|
+
export const RENAMED_FORM_VERBS: ReadonlyArray<readonly [string, string]> = [
|
|
20
|
+
['Umb.Form.Read', 'Crombolo.Form.Read'],
|
|
21
|
+
['Umb.Form.Manage', 'Crombolo.Form.Manage'],
|
|
22
|
+
['Umb.FormEntry.Read', 'Crombolo.FormEntry.Read'],
|
|
23
|
+
['Umb.FormEntry.Manage', 'Crombolo.FormEntry.Manage'],
|
|
24
|
+
['Umb.FormEntry.Sensitive', 'Crombolo.FormEntry.Sensitive'],
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export const formPermissionPrefixMigration: Migration = {
|
|
28
|
+
from: STATE_FORM_PERMISSIONS,
|
|
29
|
+
to: STATE_FORM_PERMISSION_PREFIX,
|
|
30
|
+
name: 'PrefixFormPermissionsWithCrombolo',
|
|
31
|
+
kind: 'expand',
|
|
32
|
+
release: '0.5.0',
|
|
33
|
+
up: async (db: Db) => {
|
|
34
|
+
for (const [before, after] of RENAMED_FORM_VERBS) {
|
|
35
|
+
// The delete first: a group holding both spellings would otherwise break
|
|
36
|
+
// the row's uniqueness on the way through.
|
|
37
|
+
await db.exec(
|
|
38
|
+
`DELETE FROM user_group_permission
|
|
39
|
+
WHERE permission = ? AND user_group_key IN (
|
|
40
|
+
SELECT user_group_key FROM user_group_permission WHERE permission = ?
|
|
41
|
+
)`,
|
|
42
|
+
[before, after],
|
|
43
|
+
)
|
|
44
|
+
await db.exec('UPDATE user_group_permission SET permission = ? WHERE permission = ?', [
|
|
45
|
+
after,
|
|
46
|
+
before,
|
|
47
|
+
])
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 025 — the form permission verbs, for databases seeded before they
|
|
3
|
+
* existed. `docs/18-forms.md`.
|
|
4
|
+
*
|
|
5
|
+
* The same shape as migration 007: granting what the seed now grants, so an
|
|
6
|
+
* upgraded site does not have a Forms section nobody can open. Idempotent, and
|
|
7
|
+
* it never takes a verb away.
|
|
8
|
+
*/
|
|
9
|
+
import type { Db } from '../database.ts'
|
|
10
|
+
import type { Migration } from '../migrations.ts'
|
|
11
|
+
import { STATE_FORM_WORKFLOWS } from './form-workflows.ts'
|
|
12
|
+
import { grantBuiltInPermissions } from './seed.ts'
|
|
13
|
+
|
|
14
|
+
export const STATE_FORM_PERMISSIONS = '{6f1d4a8e-0025-4e72-9a55-3d7c1b8e4f02}'
|
|
15
|
+
|
|
16
|
+
export const formPermissionsMigration: Migration = {
|
|
17
|
+
from: STATE_FORM_WORKFLOWS,
|
|
18
|
+
to: STATE_FORM_PERMISSIONS,
|
|
19
|
+
name: 'GrantFormPermissions',
|
|
20
|
+
kind: 'expand',
|
|
21
|
+
release: '0.5.0',
|
|
22
|
+
// Only the verbs: the `forms` section alias has been seeded on the admin and
|
|
23
|
+
// editor groups all along — Umbraco seeds it too — so there is nothing to add
|
|
24
|
+
// there. What was missing is a section to render, and that is code.
|
|
25
|
+
up: (db: Db) => grantBuiltInPermissions(db),
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 024 — the queue a form's workflows run from. `docs/18-forms.md`.
|
|
3
|
+
*
|
|
4
|
+
* A submission is stored and *then* its workflows run, so a mail server being
|
|
5
|
+
* down never loses an entry. That means a row per workflow per submission,
|
|
6
|
+
* claimed one node at a time and retried with a backoff.
|
|
7
|
+
*
|
|
8
|
+
* The values are carried on the row rather than read back from the entry,
|
|
9
|
+
* because a form may store no entries at all and still have workflows — the
|
|
10
|
+
* schema validator insists on exactly that, since a form that does neither has
|
|
11
|
+
* no effect.
|
|
12
|
+
*
|
|
13
|
+
* `run_on` rather than `trigger`: the latter is a reserved word in Postgres.
|
|
14
|
+
*/
|
|
15
|
+
import type { Db } from '../database.ts'
|
|
16
|
+
import type { Migration } from '../migrations.ts'
|
|
17
|
+
import { STATE_FORMS } from './forms.ts'
|
|
18
|
+
|
|
19
|
+
export const STATE_FORM_WORKFLOWS = '{6f1d4a8e-0024-4e72-9a55-3d7c1b8e4f02}'
|
|
20
|
+
|
|
21
|
+
export const formWorkflowsMigration: Migration = {
|
|
22
|
+
from: STATE_FORMS,
|
|
23
|
+
to: STATE_FORM_WORKFLOWS,
|
|
24
|
+
name: 'AddFormWorkflowRuns',
|
|
25
|
+
kind: 'expand',
|
|
26
|
+
release: '0.5.0',
|
|
27
|
+
async up(db: Db) {
|
|
28
|
+
const t = db.dialect.types
|
|
29
|
+
|
|
30
|
+
await db.exec(
|
|
31
|
+
`CREATE TABLE form_workflow_run (
|
|
32
|
+
id ${t.varchar(64)} NOT NULL,
|
|
33
|
+
form_key ${t.varchar(64)} NOT NULL,
|
|
34
|
+
form_alias ${t.varchar(255)} NOT NULL,
|
|
35
|
+
workflow_name ${t.varchar(255)} NOT NULL,
|
|
36
|
+
workflow_type ${t.varchar(32)} NOT NULL,
|
|
37
|
+
run_on ${t.varchar(16)} NOT NULL,
|
|
38
|
+
entry_id ${t.varchar(64)},
|
|
39
|
+
payload ${t.text} NOT NULL,
|
|
40
|
+
state ${t.varchar(16)} NOT NULL,
|
|
41
|
+
attempts ${t.integer} NOT NULL,
|
|
42
|
+
last_error ${t.text},
|
|
43
|
+
run_after ${t.timestamp} NOT NULL,
|
|
44
|
+
create_date ${t.timestamp} NOT NULL,
|
|
45
|
+
update_date ${t.timestamp} NOT NULL,
|
|
46
|
+
PRIMARY KEY (id)
|
|
47
|
+
)`,
|
|
48
|
+
)
|
|
49
|
+
// The only query the runner makes: what is due now.
|
|
50
|
+
await db.exec('CREATE INDEX ix_form_workflow_run_due ON form_workflow_run (state, run_after)')
|
|
51
|
+
// And what the backoffice shows against one form.
|
|
52
|
+
await db.exec('CREATE INDEX ix_form_workflow_run_form ON form_workflow_run (form_key)')
|
|
53
|
+
},
|
|
54
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 023 — form entries. `docs/18-forms.md`.
|
|
3
|
+
*
|
|
4
|
+
* The definition is a file, so there is no form table and nothing to point at:
|
|
5
|
+
* an entry carries `form_key`, the UUID from `schema/forms/*.toml`, and keeps
|
|
6
|
+
* it even when that file is deleted. The entries are the business record and
|
|
7
|
+
* must not disappear because a developer removed a form; the backoffice lists
|
|
8
|
+
* orphaned ones under the name the form last had.
|
|
9
|
+
*
|
|
10
|
+
* Values are a row each rather than a JSON blob, because they are what an
|
|
11
|
+
* entries list filters, sorts and exports by field — which is a query, not a
|
|
12
|
+
* read of the whole thing.
|
|
13
|
+
*/
|
|
14
|
+
import type { Db } from '../database.ts'
|
|
15
|
+
import type { Migration } from '../migrations.ts'
|
|
16
|
+
import { STATE_CREATED_PACKAGES } from './created-packages.ts'
|
|
17
|
+
|
|
18
|
+
export const STATE_FORMS = '{6f1d4a8e-0023-4e72-9a55-3d7c1b8e4f02}'
|
|
19
|
+
|
|
20
|
+
export const formsMigration: Migration = {
|
|
21
|
+
from: STATE_CREATED_PACKAGES,
|
|
22
|
+
to: STATE_FORMS,
|
|
23
|
+
name: 'AddFormEntries',
|
|
24
|
+
kind: 'expand',
|
|
25
|
+
release: '0.5.0',
|
|
26
|
+
async up(db: Db) {
|
|
27
|
+
const t = db.dialect.types
|
|
28
|
+
|
|
29
|
+
await db.exec(
|
|
30
|
+
`CREATE TABLE form_entry (
|
|
31
|
+
id ${t.varchar(64)} NOT NULL,
|
|
32
|
+
form_key ${t.varchar(64)} NOT NULL,
|
|
33
|
+
form_alias ${t.varchar(255)} NOT NULL,
|
|
34
|
+
state ${t.varchar(16)} NOT NULL,
|
|
35
|
+
culture ${t.varchar(14)},
|
|
36
|
+
page_key ${t.varchar(64)},
|
|
37
|
+
ip_hash ${t.varchar(64)},
|
|
38
|
+
user_agent ${t.varchar(512)},
|
|
39
|
+
spam ${t.boolean} NOT NULL,
|
|
40
|
+
create_date ${t.timestamp} NOT NULL,
|
|
41
|
+
update_date ${t.timestamp} NOT NULL,
|
|
42
|
+
PRIMARY KEY (id)
|
|
43
|
+
)`,
|
|
44
|
+
)
|
|
45
|
+
// The entries view is always "this form, newest first, within a date range",
|
|
46
|
+
// so that is the index rather than one per column.
|
|
47
|
+
await db.exec('CREATE INDEX ix_form_entry_form ON form_entry (form_key, create_date)')
|
|
48
|
+
await db.exec('CREATE INDEX ix_form_entry_state ON form_entry (form_key, state)')
|
|
49
|
+
|
|
50
|
+
await db.exec(
|
|
51
|
+
`CREATE TABLE form_entry_value (
|
|
52
|
+
entry_id ${t.varchar(64)} NOT NULL,
|
|
53
|
+
field_alias ${t.varchar(255)} NOT NULL,
|
|
54
|
+
sort_order ${t.integer} NOT NULL,
|
|
55
|
+
value ${t.text},
|
|
56
|
+
PRIMARY KEY (entry_id, field_alias, sort_order),
|
|
57
|
+
FOREIGN KEY (entry_id) REFERENCES form_entry (id) ON DELETE CASCADE
|
|
58
|
+
)`,
|
|
59
|
+
)
|
|
60
|
+
// Reading one entry's values, and filtering a list by one field's value.
|
|
61
|
+
await db.exec('CREATE INDEX ix_form_entry_value_field ON form_entry_value (field_alias)')
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 007 — the built-in groups' default permission verbs, for databases
|
|
3
|
+
* seeded before the seed granted them. Without them the backoffice hides
|
|
4
|
+
* Create, Publish, Delete and every other permission-gated action.
|
|
5
|
+
*/
|
|
6
|
+
import type { Db } from '../database.ts'
|
|
7
|
+
import type { Migration } from '../migrations.ts'
|
|
8
|
+
import { STATE_RECYCLE_BIN } from './recycle-bin.ts'
|
|
9
|
+
import { grantBuiltInPermissions } from './seed.ts'
|
|
10
|
+
|
|
11
|
+
export const STATE_GROUP_PERMISSIONS = '{6f1d4a8e-0007-4c21-9d3a-8b1e5c7a2f07}'
|
|
12
|
+
|
|
13
|
+
export const groupPermissionsMigration: Migration = {
|
|
14
|
+
from: STATE_RECYCLE_BIN,
|
|
15
|
+
to: STATE_GROUP_PERMISSIONS,
|
|
16
|
+
name: 'GrantBuiltInGroupPermissions',
|
|
17
|
+
kind: 'expand',
|
|
18
|
+
release: '0.3.0',
|
|
19
|
+
up: (db: Db) => grantBuiltInPermissions(db),
|
|
20
|
+
}
|