@crombolo/schema 0.13.1 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crombolo/schema",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
4
4
  "description": "Schema-as-code for crombolo: TOML document, media and member type definitions.",
5
5
  "license": "MIT",
6
6
  "author": "Crombolo",
@@ -28,7 +28,7 @@
28
28
  "access": "public"
29
29
  },
30
30
  "dependencies": {
31
- "@crombolo/core": "0.13.1",
32
- "@crombolo/data": "0.13.1"
31
+ "@crombolo/core": "0.14.1",
32
+ "@crombolo/data": "0.14.1"
33
33
  }
34
34
  }
package/src/export.ts CHANGED
@@ -69,6 +69,7 @@ function toSchemaProperty(p: PropertyTypeModel, ctx: ExportContext): SchemaPrope
69
69
  if (p.memberCanEdit) property.memberCanEdit = true
70
70
  if (p.isSensitive) property.sensitive = true
71
71
  if (p.searchable === false) property.searchable = false
72
+ if (p.siteSearch !== undefined && p.siteSearch !== 'normal') property.siteSearch = p.siteSearch
72
73
  return property
73
74
  }
74
75
 
@@ -109,6 +110,7 @@ export function toSchemaContentType(
109
110
  tabs: [],
110
111
  }
111
112
  if (aggregate.description) type.description = aggregate.description
113
+ if (aggregate.siteSearch === false) type.siteSearch = false
112
114
  if (aggregate.defaultComponentKey) {
113
115
  const alias = ctx.componentAlias(aggregate.defaultComponentKey)
114
116
  if (alias) type.defaultComponent = alias
package/src/model.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SchemaForm, SchemaWorkflow } from '@crombolo/core'
1
+ import type { SchemaForm, SchemaWorkflow, SiteSearchLevel } from '@crombolo/core'
2
2
 
3
3
  /**
4
4
  * The schema-as-code model: what a site's `schema/*.toml` files describe.
@@ -28,6 +28,8 @@ export interface SchemaProperty {
28
28
  sensitive?: boolean
29
29
  /** False keeps the property's values out of backoffice search; searchable when absent. */
30
30
  searchable?: boolean
31
+ /** Its weight in site search, or false to leave it out; `normal` when absent. */
32
+ siteSearch?: SiteSearchLevel | false
31
33
  /** Fills existing content when a required property is added to a type with pages. */
32
34
  default?: unknown
33
35
  since?: string
@@ -73,6 +75,8 @@ export interface SchemaContentType {
73
75
  folder?: string
74
76
  cleanup: SchemaCleanup
75
77
  since?: string
78
+ /** False leaves every page of the type out of site search. */
79
+ siteSearch?: false
76
80
  /** Properties outside any tab. */
77
81
  properties: SchemaProperty[]
78
82
  /** Groups outside any tab — `[[group]]`; Umbraco's built-in media types use one. */
package/src/parse.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * the most common mistake a non-developer makes and silently ignoring it would
4
4
  * hide the thing they were trying to do.
5
5
  */
6
+ import { SITE_SEARCH_LEVELS, type SiteSearchLevel } from '@crombolo/core'
6
7
  import {
7
8
  type SchemaContentType,
8
9
  type SchemaDataType,
@@ -23,6 +24,7 @@ const PROPERTY_KEYS = [
23
24
  'member-can-edit',
24
25
  'sensitive',
25
26
  'searchable',
27
+ 'site-search',
26
28
  'key',
27
29
  'alias',
28
30
  'name',
@@ -61,6 +63,7 @@ const TYPE_KEYS = [
61
63
  'folder',
62
64
  'cleanup',
63
65
  'since',
66
+ 'site-search',
64
67
  ] as const
65
68
  const CLEANUP_KEYS = [
66
69
  'prevent',
@@ -104,11 +107,22 @@ function property(r: Reader, t: Toml, path: string): SchemaProperty {
104
107
  : {}),
105
108
  ...(t.sensitive !== undefined ? { sensitive: r.bool(t, 'sensitive', path, false) } : {}),
106
109
  ...(t.searchable !== undefined ? { searchable: r.bool(t, 'searchable', path, true) } : {}),
110
+ ...(t['site-search'] !== undefined ? { siteSearch: siteSearchLevel(r, t, path) } : {}),
107
111
  default: t.default,
108
112
  since: r.str(t, 'since', path),
109
113
  }
110
114
  }
111
115
 
116
+ /** `site-search` on a property: a level, or false to leave it out. */
117
+ function siteSearchLevel(r: Reader, t: Toml, path: string): SiteSearchLevel | false {
118
+ const value = t['site-search']
119
+ if (value === false) return false
120
+ if (typeof value === 'string' && (SITE_SEARCH_LEVELS as readonly string[]).includes(value))
121
+ return value as SiteSearchLevel
122
+ r.problem(`${path}.site-search`, `expected false or one of ${SITE_SEARCH_LEVELS.join(', ')}`)
123
+ return 'normal'
124
+ }
125
+
112
126
  function group(r: Reader, t: Toml, path: string): SchemaGroup {
113
127
  r.unknown(t, GROUP_KEYS, path)
114
128
  return {
@@ -132,9 +146,9 @@ function tab(r: Reader, t: Toml, path: string): SchemaTab {
132
146
  }
133
147
  }
134
148
 
135
- /** Media types have no templates and no version cleanup policy. */
149
+ /** Media types have no templates, no version cleanup policy, and are not pages to search. */
136
150
  const MEDIA_TYPE_KEYS = TYPE_KEYS.filter(
137
- (k) => k !== 'components' && k !== 'default-component' && k !== 'cleanup',
151
+ (k) => k !== 'components' && k !== 'default-component' && k !== 'cleanup' && k !== 'site-search',
138
152
  )
139
153
  /** Member types, besides, allow no children: members are not a tree. */
140
154
  const MEMBER_TYPE_KEYS = MEDIA_TYPE_KEYS.filter((k) => k !== 'allow-children')
@@ -190,6 +204,9 @@ export function parseContentType(
190
204
  : undefined,
191
205
  },
192
206
  since: r.str(t, 'since', path),
207
+ ...(t['site-search'] !== undefined && typeSiteSearch(r, t, path) === false
208
+ ? { siteSearch: false as const }
209
+ : {}),
193
210
  properties: r.tables(root.property, 'property').map((p, i) => property(r, p, `property[${i}]`)),
194
211
  tabs: r.tables(root.tab, 'tab').map((x, i) => tab(r, x, `tab[${i}]`)),
195
212
  groups: r.tables(root.group, 'group').map((g, i) => group(r, g, `group[${i}]`)),
@@ -197,6 +214,13 @@ export function parseContentType(
197
214
  return { value: r.problems.length === 0 ? value : undefined, problems: r.problems }
198
215
  }
199
216
 
217
+ /** `site-search` on a content type: only false, which leaves its pages out. */
218
+ function typeSiteSearch(r: Reader, t: Toml, path: string): false | undefined {
219
+ if (t['site-search'] === false) return false
220
+ r.problem(`${path}.site-search`, 'expected false; a page type is searchable unless it says so')
221
+ return undefined
222
+ }
223
+
200
224
  export function parseMediaType(file: string, source: string): ParseResult<SchemaContentType> {
201
225
  return parseContentType(file, source, 'media')
202
226
  }
package/src/sync.ts CHANGED
@@ -240,8 +240,15 @@ export async function syncSchema(
240
240
  for (const d of set.dataTypes) {
241
241
  const existing = await dataTypes.byAlias(d.alias)
242
242
  const builtin = BUILTIN_DATA_TYPES.find((b) => b.alias === d.alias)
243
- const key = d.key ?? existing?.key ?? crypto.randomUUID()
244
- if (!d.key && !existing)
243
+ // A key this site has never had, for an alias it has, is that data
244
+ // type: the row keeps its key and the file is given it (story 105).
245
+ const rekeyed =
246
+ d.key !== undefined &&
247
+ existing !== undefined &&
248
+ existing.key.toLowerCase() !== d.key.toLowerCase() &&
249
+ !(await dataTypes.byKey(d.key))
250
+ const key = (rekeyed ? existing?.key : d.key) ?? existing?.key ?? crypto.randomUUID()
251
+ if ((!d.key && !existing) || rekeyed)
245
252
  report.assignedKeys.push({
246
253
  file: loaded.files.get(`data-type:${d.alias}`) ?? '',
247
254
  typeAlias: d.alias,
@@ -370,17 +377,35 @@ export async function syncSchema(
370
377
  type: SchemaContentType
371
378
  aggregate: ContentTypeAggregate
372
379
  }> = []
380
+ const fileTypeKeys = new Set(
381
+ kind.types.flatMap((t) => (t.key ? [t.key.toLowerCase()] : [])),
382
+ )
373
383
  for (const t of inCompositionOrder(kind.types)) {
374
384
  const file = loaded.files.get(typeFileKey(kind.name, t.alias)) ?? ''
375
- const existing = t.key
385
+ let existing = t.key
376
386
  ? await kind.repo.byKey(t.key)
377
387
  : await kind.repo.byAlias(t.alias, { includeRetired: true })
388
+ // A key this site has never had, for an alias it has and no other
389
+ // file claims, is that type — as a property's is: the row keeps its
390
+ // key, and the file is given it (story 105).
391
+ let rekeyed = false
392
+ if (!existing && t.key) {
393
+ const sameAlias = await kind.repo.byAlias(t.alias, { includeRetired: true })
394
+ if (sameAlias && !fileTypeKeys.has(sameAlias.key.toLowerCase())) {
395
+ existing = sameAlias
396
+ rekeyed = true
397
+ }
398
+ }
378
399
  const shippedKey =
379
400
  kind.name === 'document'
380
401
  ? SYSTEM_CONTENT_TYPES.find((d) => d.alias === t.alias)?.key
381
402
  : undefined
382
- const key = t.key ?? existing?.key ?? shippedKey ?? crypto.randomUUID()
383
- if (!t.key && !existing)
403
+ const key =
404
+ (rekeyed ? existing?.key : t.key) ??
405
+ existing?.key ??
406
+ shippedKey ??
407
+ crypto.randomUUID()
408
+ if ((!t.key && !existing) || rekeyed)
384
409
  report.assignedKeys.push({ file, typeAlias: t.alias, key, revived: false })
385
410
  typeKeyByAlias.set(t.alias, key)
386
411
  const retired = existing ? await kind.repo.retiredProperties(existing.key) : []
@@ -454,6 +479,7 @@ export async function syncSchema(
454
479
  memberCanEdit: p.memberCanEdit === true,
455
480
  isSensitive: p.sensitive === true,
456
481
  searchable: p.searchable !== false,
482
+ siteSearch: p.siteSearch ?? 'normal',
457
483
  }
458
484
  }
459
485
  // Groups have no key in the file; they match an existing group of the same name.
@@ -524,6 +550,7 @@ export async function syncSchema(
524
550
  allowedInLibrary: t.allowInLibrary,
525
551
  collectionKey: t.collection ? (dataTypeKeyByAlias.get(t.collection) ?? null) : null,
526
552
  sinceVersion: t.since ?? null,
553
+ siteSearch: t.siteSearch !== false,
527
554
  cleanup: {
528
555
  preventCleanup: t.cleanup.prevent,
529
556
  keepAllVersionsNewerThanDays: t.cleanup.keepAllNewerThanDays ?? null,
package/src/write.ts CHANGED
@@ -67,6 +67,7 @@ function writeProperty(out: string[], header: string, p: SchemaProperty): void {
67
67
  line(out, 'member-can-edit', true, p.memberCanEdit === true)
68
68
  line(out, 'sensitive', true, p.sensitive === true)
69
69
  line(out, 'searchable', false, p.searchable === false)
70
+ line(out, 'site-search', p.siteSearch, p.siteSearch !== 'normal')
70
71
  line(out, 'default', p.default)
71
72
  line(out, 'since', p.since)
72
73
  }
@@ -91,6 +92,7 @@ export function writeContentType(t: SchemaContentType, kind: SchemaTypeKind = 'd
91
92
  line(out, 'collection', t.collection)
92
93
  line(out, 'folder', t.folder)
93
94
  line(out, 'since', t.since)
95
+ line(out, 'site-search', false, t.siteSearch === false)
94
96
  if (
95
97
  t.cleanup.prevent ||
96
98
  t.cleanup.keepAllNewerThanDays !== undefined ||