@crombolo/schema 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 +32 -4
- package/src/builtin.ts +215 -0
- package/src/check.ts +502 -0
- package/src/export.ts +419 -0
- package/src/forms.ts +328 -0
- package/src/generate.ts +149 -0
- package/src/index.ts +19 -0
- package/src/load.ts +137 -0
- package/src/model.ts +156 -0
- package/src/parse-forms.ts +229 -0
- package/src/parse.ts +260 -0
- package/src/reader.ts +99 -0
- package/src/scaffold.ts +172 -0
- package/src/sync.ts +615 -0
- package/src/upgrade.ts +296 -0
- package/src/validate-forms.ts +346 -0
- package/src/validate.ts +242 -0
- package/src/value-migrations.ts +122 -0
- package/src/version.ts +53 -0
- package/src/write-forms.ts +139 -0
- package/src/write.ts +176 -0
- package/README.md +0 -3
package/src/export.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database → files. `exportSchemaSet` reads the current definitions as the
|
|
3
|
+
* schema model; `writeSchemaFiles` lays them out canonically; `writeKeysBack`
|
|
4
|
+
* records the keys a sync minted so the next deploy carries them.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import {
|
|
9
|
+
type ContentTypeAggregate,
|
|
10
|
+
type DataTypeModel,
|
|
11
|
+
ObjectTypes,
|
|
12
|
+
type PropertyTypeModel,
|
|
13
|
+
} from '@crombolo/core'
|
|
14
|
+
import {
|
|
15
|
+
ComponentRepository,
|
|
16
|
+
ContentTypeRepository,
|
|
17
|
+
DataTypeRepository,
|
|
18
|
+
type Db,
|
|
19
|
+
FolderRepository,
|
|
20
|
+
LanguageRepository,
|
|
21
|
+
SYSTEM_CONTENT_TYPES,
|
|
22
|
+
SYSTEM_MEDIA_TYPES,
|
|
23
|
+
} from '@crombolo/data'
|
|
24
|
+
import { BUILTIN_DATA_TYPES, builtInContentTypeSchema, builtInMediaTypeSchema } from './builtin.ts'
|
|
25
|
+
import type { LoadedSchema } from './load.ts'
|
|
26
|
+
import type {
|
|
27
|
+
SchemaContentType,
|
|
28
|
+
SchemaDataType,
|
|
29
|
+
SchemaLanguage,
|
|
30
|
+
SchemaProperty,
|
|
31
|
+
SchemaSet,
|
|
32
|
+
} from './model.ts'
|
|
33
|
+
import { TYPE_KIND_FILES, typeFileKey } from './model.ts'
|
|
34
|
+
import type { AssignedKey } from './sync.ts'
|
|
35
|
+
import {
|
|
36
|
+
fileNameFor,
|
|
37
|
+
writeContentType,
|
|
38
|
+
writeDataType,
|
|
39
|
+
writeLanguages,
|
|
40
|
+
writeMediaType,
|
|
41
|
+
writeMemberType,
|
|
42
|
+
writeSchemaVersion,
|
|
43
|
+
} from './write.ts'
|
|
44
|
+
|
|
45
|
+
/** Key → alias lookups for everything a content type refers to. */
|
|
46
|
+
export interface ExportContext {
|
|
47
|
+
dataTypeAlias(key: string): string | undefined
|
|
48
|
+
contentTypeAlias(key: string): string | undefined
|
|
49
|
+
componentAlias(key: string): string | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toSchemaProperty(p: PropertyTypeModel, ctx: ExportContext): SchemaProperty {
|
|
53
|
+
const property: SchemaProperty = {
|
|
54
|
+
key: p.key,
|
|
55
|
+
alias: p.alias,
|
|
56
|
+
name: p.name,
|
|
57
|
+
type: ctx.dataTypeAlias(p.dataTypeKey) ?? p.dataTypeKey,
|
|
58
|
+
mandatory: p.mandatory,
|
|
59
|
+
variesByCulture: p.variesByCulture,
|
|
60
|
+
variesBySegment: p.variesBySegment,
|
|
61
|
+
labelOnTop: p.labelOnTop,
|
|
62
|
+
}
|
|
63
|
+
if (p.description) property.description = p.description
|
|
64
|
+
if (p.mandatoryMessage) property.mandatoryMessage = p.mandatoryMessage
|
|
65
|
+
if (p.regEx) property.regex = p.regEx
|
|
66
|
+
if (p.regExMessage) property.regexMessage = p.regExMessage
|
|
67
|
+
if (p.memberCanView) property.memberCanView = true
|
|
68
|
+
if (p.memberCanEdit) property.memberCanEdit = true
|
|
69
|
+
if (p.isSensitive) property.sensitive = true
|
|
70
|
+
return property
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function toSchemaContentType(
|
|
74
|
+
aggregate: ContentTypeAggregate,
|
|
75
|
+
ctx: ExportContext,
|
|
76
|
+
): SchemaContentType {
|
|
77
|
+
const byOrder = (a: { sortOrder: number }, b: { sortOrder: number }) => a.sortOrder - b.sortOrder
|
|
78
|
+
const properties = [...aggregate.properties].sort(byOrder)
|
|
79
|
+
const containers = [...aggregate.containers].sort(byOrder)
|
|
80
|
+
const under = (containerKey: string | null) =>
|
|
81
|
+
properties
|
|
82
|
+
.filter((p) => (p.containerKey ?? null) === containerKey)
|
|
83
|
+
.map((p) => toSchemaProperty(p, ctx))
|
|
84
|
+
|
|
85
|
+
const type: SchemaContentType = {
|
|
86
|
+
key: aggregate.key,
|
|
87
|
+
alias: aggregate.alias,
|
|
88
|
+
name: aggregate.name,
|
|
89
|
+
icon: aggregate.icon,
|
|
90
|
+
allowAtRoot: aggregate.allowedAsRoot,
|
|
91
|
+
isElement: aggregate.isElement,
|
|
92
|
+
allowInLibrary: aggregate.allowedInLibrary,
|
|
93
|
+
variesByCulture: aggregate.variesByCulture,
|
|
94
|
+
variesBySegment: aggregate.variesBySegment,
|
|
95
|
+
compositions: aggregate.compositions
|
|
96
|
+
.map((c) => ctx.contentTypeAlias(c.contentTypeKey))
|
|
97
|
+
.filter((alias): alias is string => alias !== undefined),
|
|
98
|
+
allowChildren: [...aggregate.allowedContentTypes]
|
|
99
|
+
.sort(byOrder)
|
|
100
|
+
.map((c) => ctx.contentTypeAlias(c.contentTypeKey))
|
|
101
|
+
.filter((alias): alias is string => alias !== undefined),
|
|
102
|
+
components: aggregate.allowedComponentKeys
|
|
103
|
+
.map((key) => ctx.componentAlias(key))
|
|
104
|
+
.filter((alias): alias is string => alias !== undefined),
|
|
105
|
+
cleanup: { prevent: aggregate.cleanup.preventCleanup },
|
|
106
|
+
properties: under(null),
|
|
107
|
+
tabs: [],
|
|
108
|
+
}
|
|
109
|
+
if (aggregate.description) type.description = aggregate.description
|
|
110
|
+
if (aggregate.defaultComponentKey) {
|
|
111
|
+
const alias = ctx.componentAlias(aggregate.defaultComponentKey)
|
|
112
|
+
if (alias) type.defaultComponent = alias
|
|
113
|
+
}
|
|
114
|
+
if (aggregate.collectionKey) {
|
|
115
|
+
const alias = ctx.dataTypeAlias(aggregate.collectionKey)
|
|
116
|
+
if (alias) type.collection = alias
|
|
117
|
+
}
|
|
118
|
+
if (aggregate.cleanup.keepAllVersionsNewerThanDays !== null)
|
|
119
|
+
type.cleanup.keepAllNewerThanDays = aggregate.cleanup.keepAllVersionsNewerThanDays
|
|
120
|
+
if (aggregate.cleanup.keepLatestVersionPerDayForDays !== null)
|
|
121
|
+
type.cleanup.keepLatestPerDayForDays = aggregate.cleanup.keepLatestVersionPerDayForDays
|
|
122
|
+
|
|
123
|
+
// Umbraco allows a group without a tab: that is a top-level `[[group]]`.
|
|
124
|
+
const tabs = containers.filter((c) => c.type === 'Tab')
|
|
125
|
+
const orphanGroups = containers.filter((c) => c.type === 'Group' && !c.parentKey)
|
|
126
|
+
for (const tab of tabs) {
|
|
127
|
+
const groups = containers.filter((c) => c.type === 'Group' && c.parentKey === tab.key)
|
|
128
|
+
const entry: SchemaContentType['tabs'][number] = {
|
|
129
|
+
name: tab.name ?? '',
|
|
130
|
+
properties: under(tab.key),
|
|
131
|
+
groups: groups.map((g) => {
|
|
132
|
+
const group: SchemaContentType['tabs'][number]['groups'][number] = {
|
|
133
|
+
name: g.name ?? '',
|
|
134
|
+
properties: under(g.key),
|
|
135
|
+
}
|
|
136
|
+
if (g.alias) group.alias = g.alias
|
|
137
|
+
return group
|
|
138
|
+
}),
|
|
139
|
+
}
|
|
140
|
+
if (tab.alias) entry.alias = tab.alias
|
|
141
|
+
type.tabs.push(entry)
|
|
142
|
+
}
|
|
143
|
+
if (orphanGroups.length > 0) {
|
|
144
|
+
type.groups = orphanGroups.map((g) => {
|
|
145
|
+
const group: NonNullable<SchemaContentType['groups']>[number] = {
|
|
146
|
+
name: g.name ?? '',
|
|
147
|
+
properties: under(g.key),
|
|
148
|
+
}
|
|
149
|
+
if (g.alias) group.alias = g.alias
|
|
150
|
+
return group
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
return type
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function toSchemaDataType(model: DataTypeModel, alias: string): SchemaDataType {
|
|
157
|
+
const dataType: SchemaDataType = {
|
|
158
|
+
key: model.key,
|
|
159
|
+
alias,
|
|
160
|
+
name: model.name,
|
|
161
|
+
editor: model.editorAlias,
|
|
162
|
+
config: Object.fromEntries(model.values.map((v) => [v.alias, v.value])),
|
|
163
|
+
}
|
|
164
|
+
if (model.editorUiAlias) dataType.editorUi = model.editorUiAlias
|
|
165
|
+
return dataType
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A data type created in the backoffice has no alias; derive one from its name. */
|
|
169
|
+
export function aliasFromName(name: string): string {
|
|
170
|
+
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean)
|
|
171
|
+
const camel = words
|
|
172
|
+
.map((w, i) =>
|
|
173
|
+
i === 0 ? w.charAt(0).toLowerCase() + w.slice(1) : w.charAt(0).toUpperCase() + w.slice(1),
|
|
174
|
+
)
|
|
175
|
+
.join('')
|
|
176
|
+
return /^[A-Za-z]/.test(camel) ? camel : `dataType${camel}`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A built-in needs no file unless the site changed its editor or configuration. */
|
|
180
|
+
function isUnchangedBuiltin(dataType: SchemaDataType): boolean {
|
|
181
|
+
const builtin = BUILTIN_DATA_TYPES.find((b) => b.alias === dataType.alias)
|
|
182
|
+
if (!builtin) return false
|
|
183
|
+
const config = (c: Record<string, unknown>) =>
|
|
184
|
+
JSON.stringify(Object.fromEntries(Object.entries(c).sort(([a], [b]) => a.localeCompare(b))))
|
|
185
|
+
return builtin.editor === dataType.editor && config(builtin.config) === config(dataType.config)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Every live definition in the database as a schema set; keys are always present. */
|
|
189
|
+
export async function exportSchemaSet(db: Db, version: string): Promise<SchemaSet> {
|
|
190
|
+
const types = new ContentTypeRepository(db)
|
|
191
|
+
const dataTypes = new DataTypeRepository(db)
|
|
192
|
+
const templates = new ComponentRepository(db)
|
|
193
|
+
|
|
194
|
+
const mediaTypeRepo = new ContentTypeRepository(db, { kind: 'media' })
|
|
195
|
+
const liveTypes = async (repo: ContentTypeRepository) => {
|
|
196
|
+
const rows = await db.query<{ unique_id: string }>(
|
|
197
|
+
`SELECT n.unique_id FROM content_type ct JOIN node n ON n.id = ct.node_id
|
|
198
|
+
WHERE ct.retired_at IS NULL AND n.node_object_type = ? ORDER BY ct.alias`,
|
|
199
|
+
[repo.objectType],
|
|
200
|
+
)
|
|
201
|
+
const out: ContentTypeAggregate[] = []
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
const aggregate = await repo.byKey(String(row.unique_id))
|
|
204
|
+
if (aggregate) out.push(aggregate)
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
const aggregates = await liveTypes(types)
|
|
209
|
+
const mediaAggregates = await liveTypes(mediaTypeRepo)
|
|
210
|
+
const memberAggregates = await liveTypes(new ContentTypeRepository(db, { kind: 'member' }))
|
|
211
|
+
|
|
212
|
+
const dataTypeRows = await db.query<{ unique_id: string }>(
|
|
213
|
+
'SELECT n.unique_id FROM data_type d JOIN node n ON n.id = d.node_id ORDER BY n.text',
|
|
214
|
+
)
|
|
215
|
+
const dataTypeAliasByKey = new Map<string, string>()
|
|
216
|
+
const exportedDataTypes: SchemaDataType[] = []
|
|
217
|
+
const taken = new Set<string>()
|
|
218
|
+
for (const row of dataTypeRows) {
|
|
219
|
+
const model = await dataTypes.byKey(String(row.unique_id))
|
|
220
|
+
if (!model) continue
|
|
221
|
+
let alias = model.alias
|
|
222
|
+
if (!alias) {
|
|
223
|
+
alias = aliasFromName(model.name)
|
|
224
|
+
while (taken.has(alias)) alias = `${alias}2`
|
|
225
|
+
// Recorded so the next export and the sync agree on the name.
|
|
226
|
+
await dataTypes.save({ ...model, alias })
|
|
227
|
+
}
|
|
228
|
+
taken.add(alias)
|
|
229
|
+
dataTypeAliasByKey.set(model.key, alias)
|
|
230
|
+
const dataType = toSchemaDataType(model, alias)
|
|
231
|
+
if (!isUnchangedBuiltin(dataType)) exportedDataTypes.push(dataType)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const templateAliasByKey = new Map<string, string>()
|
|
235
|
+
const templatePage = await templates.list(0, 10_000)
|
|
236
|
+
for (const template of templatePage.items) templateAliasByKey.set(template.key, template.alias)
|
|
237
|
+
const contentTypeAliasByKey = new Map(
|
|
238
|
+
[...aggregates, ...mediaAggregates, ...memberAggregates].map((a) => [a.key, a.alias]),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const typeFolders = new FolderRepository(
|
|
242
|
+
db,
|
|
243
|
+
ObjectTypes.DocumentTypeContainer,
|
|
244
|
+
ObjectTypes.DocumentType,
|
|
245
|
+
)
|
|
246
|
+
const dataTypeFolders = new FolderRepository(
|
|
247
|
+
db,
|
|
248
|
+
ObjectTypes.DataTypeContainer,
|
|
249
|
+
ObjectTypes.DataType,
|
|
250
|
+
)
|
|
251
|
+
const folderOf = async (repo: FolderRepository, key: string) => {
|
|
252
|
+
const path = await repo.pathOf(key)
|
|
253
|
+
return path.length > 0 ? path.join('/') : undefined
|
|
254
|
+
}
|
|
255
|
+
for (const d of exportedDataTypes) {
|
|
256
|
+
const folder = await folderOf(dataTypeFolders, d.key as string)
|
|
257
|
+
if (folder) d.folder = folder
|
|
258
|
+
}
|
|
259
|
+
const ctx: ExportContext = {
|
|
260
|
+
dataTypeAlias: (key) => dataTypeAliasByKey.get(key.toLowerCase()),
|
|
261
|
+
contentTypeAlias: (key) => contentTypeAliasByKey.get(key.toLowerCase()),
|
|
262
|
+
componentAlias: (key) => templateAliasByKey.get(key.toLowerCase()),
|
|
263
|
+
}
|
|
264
|
+
const languages: SchemaLanguage[] = (await new LanguageRepository(db).all()).map((l) => {
|
|
265
|
+
const language: SchemaLanguage = {
|
|
266
|
+
iso: l.isoCode,
|
|
267
|
+
name: l.cultureName,
|
|
268
|
+
default: l.isDefault,
|
|
269
|
+
mandatory: l.isMandatory,
|
|
270
|
+
}
|
|
271
|
+
if (l.fallbackIsoCode) language.fallback = l.fallbackIsoCode
|
|
272
|
+
return language
|
|
273
|
+
})
|
|
274
|
+
const contentTypes: SchemaContentType[] = []
|
|
275
|
+
for (const a of aggregates) {
|
|
276
|
+
const type = toSchemaContentType(a, ctx)
|
|
277
|
+
const folder = await folderOf(typeFolders, a.key)
|
|
278
|
+
if (folder) type.folder = folder
|
|
279
|
+
// A system type needs a file only once a site has extended it.
|
|
280
|
+
const shipped = SYSTEM_CONTENT_TYPES.find((t) => t.key === a.key)
|
|
281
|
+
if (shipped && writeContentType(type) === writeContentType(builtInContentTypeSchema(shipped)))
|
|
282
|
+
continue
|
|
283
|
+
contentTypes.push(type)
|
|
284
|
+
}
|
|
285
|
+
const mediaTypeFolders = new FolderRepository(
|
|
286
|
+
db,
|
|
287
|
+
ObjectTypes.MediaTypeContainer,
|
|
288
|
+
ObjectTypes.MediaType,
|
|
289
|
+
)
|
|
290
|
+
const mediaTypes: SchemaContentType[] = []
|
|
291
|
+
for (const a of mediaAggregates) {
|
|
292
|
+
// Media types carry neither templates nor a cleanup policy.
|
|
293
|
+
const type = toSchemaContentType(
|
|
294
|
+
{ ...a, allowedComponentKeys: [], defaultComponentKey: null },
|
|
295
|
+
ctx,
|
|
296
|
+
)
|
|
297
|
+
type.cleanup = { prevent: false }
|
|
298
|
+
const folder = await folderOf(mediaTypeFolders, a.key)
|
|
299
|
+
if (folder) type.folder = folder
|
|
300
|
+
const shipped = SYSTEM_MEDIA_TYPES.find((t) => t.key === a.key)
|
|
301
|
+
if (shipped) {
|
|
302
|
+
const present = new Set(mediaAggregates.map((m) => m.alias))
|
|
303
|
+
if (writeMediaType(type) === writeMediaType(builtInMediaTypeSchema(shipped, present)))
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
mediaTypes.push(type)
|
|
307
|
+
}
|
|
308
|
+
const memberTypeFolders = new FolderRepository(
|
|
309
|
+
db,
|
|
310
|
+
ObjectTypes.MemberTypeContainer,
|
|
311
|
+
ObjectTypes.MemberType,
|
|
312
|
+
)
|
|
313
|
+
const memberTypes: SchemaContentType[] = []
|
|
314
|
+
for (const a of memberAggregates) {
|
|
315
|
+
// Member types carry neither templates, a cleanup policy, nor children.
|
|
316
|
+
const type = toSchemaContentType(
|
|
317
|
+
{ ...a, allowedComponentKeys: [], defaultComponentKey: null, allowedContentTypes: [] },
|
|
318
|
+
ctx,
|
|
319
|
+
)
|
|
320
|
+
type.cleanup = { prevent: false }
|
|
321
|
+
const folder = await folderOf(memberTypeFolders, a.key)
|
|
322
|
+
if (folder) type.folder = folder
|
|
323
|
+
memberTypes.push(type)
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
version,
|
|
327
|
+
contentTypes,
|
|
328
|
+
mediaTypes,
|
|
329
|
+
memberTypes,
|
|
330
|
+
dataTypes: exportedDataTypes,
|
|
331
|
+
languages,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Writes a set as canonical files; returns the paths written. Never deletes. */
|
|
336
|
+
export function writeSchemaFiles(schemaDir: string, set: SchemaSet): string[] {
|
|
337
|
+
const written: string[] = []
|
|
338
|
+
const put = (relative: string, content: string) => {
|
|
339
|
+
const file = join(schemaDir, relative)
|
|
340
|
+
mkdirSync(join(file, '..'), { recursive: true })
|
|
341
|
+
writeFileSync(file, content)
|
|
342
|
+
written.push(file)
|
|
343
|
+
}
|
|
344
|
+
put('schema.toml', writeSchemaVersion(set.version))
|
|
345
|
+
for (const t of set.contentTypes)
|
|
346
|
+
put(join(TYPE_KIND_FILES.document.dir, fileNameFor(t.alias)), writeContentType(t))
|
|
347
|
+
for (const t of set.mediaTypes ?? [])
|
|
348
|
+
put(join('media-types', fileNameFor(t.alias)), writeMediaType(t))
|
|
349
|
+
for (const t of set.memberTypes ?? [])
|
|
350
|
+
put(join('member-types', fileNameFor(t.alias)), writeMemberType(t))
|
|
351
|
+
for (const d of set.dataTypes) put(join('data-types', fileNameFor(d.alias)), writeDataType(d))
|
|
352
|
+
if (set.languages.length > 0) put('languages.toml', writeLanguages(set.languages))
|
|
353
|
+
return written
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Rewrites, canonically, every file a sync assigned keys in. */
|
|
357
|
+
export function writeKeysBack(loaded: LoadedSchema, assigned: readonly AssignedKey[]): string[] {
|
|
358
|
+
const touched = new Set<string>()
|
|
359
|
+
for (const a of assigned) {
|
|
360
|
+
const allTypes = [
|
|
361
|
+
...loaded.set.contentTypes,
|
|
362
|
+
...(loaded.set.mediaTypes ?? []),
|
|
363
|
+
...(loaded.set.memberTypes ?? []),
|
|
364
|
+
]
|
|
365
|
+
if (a.propertyAlias === undefined) {
|
|
366
|
+
const type = allTypes.find((t) => t.alias === a.typeAlias)
|
|
367
|
+
if (type) type.key = a.key
|
|
368
|
+
const dataType = loaded.set.dataTypes.find((d) => d.alias === a.typeAlias)
|
|
369
|
+
if (dataType) dataType.key = a.key
|
|
370
|
+
} else {
|
|
371
|
+
const type = allTypes.find((t) => t.alias === a.typeAlias)
|
|
372
|
+
const property = type && allPropertiesOf(type).find((p) => p.alias === a.propertyAlias)
|
|
373
|
+
if (property) property.key = a.key
|
|
374
|
+
}
|
|
375
|
+
if (a.file) touched.add(a.file)
|
|
376
|
+
}
|
|
377
|
+
const written: string[] = []
|
|
378
|
+
for (const file of touched) {
|
|
379
|
+
const type = loaded.set.contentTypes.find((t) => loaded.files.get(t.alias) === file)
|
|
380
|
+
if (type) {
|
|
381
|
+
writeFileSync(file, writeContentType(type))
|
|
382
|
+
written.push(file)
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
const mediaType = (loaded.set.mediaTypes ?? []).find(
|
|
386
|
+
(t) => loaded.files.get(typeFileKey('media', t.alias)) === file,
|
|
387
|
+
)
|
|
388
|
+
if (mediaType) {
|
|
389
|
+
writeFileSync(file, writeMediaType(mediaType))
|
|
390
|
+
written.push(file)
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
const memberType = (loaded.set.memberTypes ?? []).find(
|
|
394
|
+
(t) => loaded.files.get(typeFileKey('member', t.alias)) === file,
|
|
395
|
+
)
|
|
396
|
+
if (memberType) {
|
|
397
|
+
writeFileSync(file, writeMemberType(memberType))
|
|
398
|
+
written.push(file)
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
const dataType = loaded.set.dataTypes.find(
|
|
402
|
+
(d) => loaded.files.get(`data-type:${d.alias}`) === file,
|
|
403
|
+
)
|
|
404
|
+
if (dataType) {
|
|
405
|
+
writeFileSync(file, writeDataType(dataType))
|
|
406
|
+
written.push(file)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return written
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function allPropertiesOf(type: SchemaContentType): SchemaProperty[] {
|
|
413
|
+
const out = [...type.properties]
|
|
414
|
+
for (const tab of type.tabs) {
|
|
415
|
+
out.push(...tab.properties)
|
|
416
|
+
for (const group of tab.groups) out.push(...group.properties)
|
|
417
|
+
}
|
|
418
|
+
return out
|
|
419
|
+
}
|