@crombolo/data 0.10.0 → 0.12.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 +2 -2
- package/src/ambient.ts +20 -0
- package/src/audit.ts +291 -0
- package/src/busy.ts +74 -0
- package/src/change-report.ts +6 -0
- package/src/cluster.ts +17 -5
- package/src/database.ts +80 -17
- package/src/dialect.ts +24 -0
- package/src/index.ts +20 -0
- package/src/locks.ts +94 -25
- package/src/migrations.ts +42 -16
- package/src/relations.ts +346 -0
- package/src/repositories/content-types.ts +41 -4
- package/src/repositories/data-types.ts +38 -0
- package/src/repositories/delivery-member-tokens.ts +110 -0
- package/src/repositories/dictionary.ts +18 -0
- package/src/repositories/documents.ts +443 -88
- package/src/repositories/dynamic-roots.ts +151 -0
- package/src/repositories/form-entries.ts +32 -11
- package/src/repositories/form-workflows.ts +1 -1
- package/src/repositories/members.ts +26 -1
- package/src/repositories/nodes.ts +43 -7
- package/src/repositories/public-access.ts +30 -0
- package/src/repositories/references.ts +15 -0
- package/src/repositories/transfer-answers.ts +151 -0
- package/src/repositories/webhooks.ts +299 -0
- package/src/schema/audit-log.ts +45 -0
- package/src/schema/delivery-member-tokens.ts +40 -0
- package/src/schema/diagnostics.ts +28 -0
- package/src/schema/plan.ts +14 -0
- package/src/schema/property-searchable.ts +29 -0
- package/src/schema/search-index.ts +92 -0
- package/src/schema/seed.ts +13 -2
- package/src/schema/transfer-answers.ts +50 -0
- package/src/schema/webhooks.ts +78 -0
- package/src/search-text.ts +133 -0
- package/src/search.ts +397 -0
- package/src/setup.ts +57 -0
- package/src/tree-invariants.ts +167 -0
- package/src/url-options.ts +61 -0
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { appendCacheInstruction } from '../cluster.ts'
|
|
28
28
|
import type { Db } from '../database.ts'
|
|
29
29
|
import { DbDate, fromDbBool } from '../dialect.ts'
|
|
30
|
-
import { Locks } from '../locks.ts'
|
|
30
|
+
import { type LockId, Locks } from '../locks.ts'
|
|
31
31
|
import { isPending } from '../pending.ts'
|
|
32
32
|
import {
|
|
33
33
|
assertNodeMayWrite,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type SchemaStateRow,
|
|
37
37
|
visibleStateIdFor,
|
|
38
38
|
} from '../schema-state.ts'
|
|
39
|
+
import { queueForSearch } from '../search.ts'
|
|
39
40
|
import { NodeRepository, type NodeRow } from './nodes.ts'
|
|
40
41
|
|
|
41
42
|
export type EventKind = 'save' | 'publish' | 'rollback' | 'migrate'
|
|
@@ -85,6 +86,13 @@ export interface MigratedValue {
|
|
|
85
86
|
storage?: ValueStorageTypeName
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
interface PublishedCulture {
|
|
90
|
+
languageId: number
|
|
91
|
+
isoCode: string
|
|
92
|
+
eventId: number
|
|
93
|
+
name: string
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
interface ValueRow {
|
|
89
97
|
eventId: number
|
|
90
98
|
id: number
|
|
@@ -132,17 +140,24 @@ export type ContentKind = 'document' | 'media' | 'member' | 'blueprint' | 'eleme
|
|
|
132
140
|
|
|
133
141
|
const KINDS: Record<
|
|
134
142
|
ContentKind,
|
|
135
|
-
{
|
|
143
|
+
{
|
|
144
|
+
objectType: string
|
|
145
|
+
recycleBin: number | undefined
|
|
146
|
+
container: string | undefined
|
|
147
|
+
lock: LockId
|
|
148
|
+
}
|
|
136
149
|
> = {
|
|
137
150
|
document: {
|
|
138
151
|
objectType: ObjectTypes.Document,
|
|
139
152
|
recycleBin: SystemNodes.ContentRecycleBin,
|
|
140
153
|
container: undefined,
|
|
154
|
+
lock: Locks.ContentTree,
|
|
141
155
|
},
|
|
142
156
|
media: {
|
|
143
157
|
objectType: ObjectTypes.Media,
|
|
144
158
|
recycleBin: SystemNodes.MediaRecycleBin,
|
|
145
159
|
container: undefined,
|
|
160
|
+
lock: Locks.MediaTree,
|
|
146
161
|
},
|
|
147
162
|
// Members are a flat list at the tree root: no bin to trash them into, and
|
|
148
163
|
// no folders to gather them in.
|
|
@@ -150,11 +165,13 @@ const KINDS: Record<
|
|
|
150
165
|
objectType: ObjectTypes.Member,
|
|
151
166
|
recycleBin: undefined,
|
|
152
167
|
container: undefined,
|
|
168
|
+
lock: Locks.MemberTree,
|
|
153
169
|
},
|
|
154
170
|
blueprint: {
|
|
155
171
|
objectType: ObjectTypes.DocumentBlueprint,
|
|
156
172
|
recycleBin: undefined,
|
|
157
173
|
container: ObjectTypes.DocumentBlueprintContainer,
|
|
174
|
+
lock: Locks.ContentTree,
|
|
158
175
|
},
|
|
159
176
|
// Umbraco 18's Elements: publishable content with no URL, gathered in folders
|
|
160
177
|
// and trashed into a bin of its own at -22, as its Constants.System says.
|
|
@@ -162,6 +179,7 @@ const KINDS: Record<
|
|
|
162
179
|
objectType: ObjectTypes.Element,
|
|
163
180
|
recycleBin: SystemNodes.ElementRecycleBin,
|
|
164
181
|
container: ObjectTypes.ElementContainer,
|
|
182
|
+
lock: Locks.ContentTree,
|
|
165
183
|
},
|
|
166
184
|
}
|
|
167
185
|
|
|
@@ -200,6 +218,9 @@ export interface DocumentRepositoryOptions {
|
|
|
200
218
|
/** Thrown when a publish is refused by a business rule rather than failing. */
|
|
201
219
|
export class PublishBlockedError extends Error {}
|
|
202
220
|
|
|
221
|
+
/** The parent named for a new item is gone, or in the recycle bin. */
|
|
222
|
+
export class InvalidParentError extends Error {}
|
|
223
|
+
|
|
203
224
|
const isEmpty = (value: unknown) =>
|
|
204
225
|
value === null || value === undefined || (typeof value === 'string' && value.trim() === '')
|
|
205
226
|
|
|
@@ -249,6 +270,39 @@ function validateValues(
|
|
|
249
270
|
const keyOf = (propertyTypeId: number, languageId: number | null, segment: string | null) =>
|
|
250
271
|
`${propertyTypeId}|${languageId ?? ''}|${segment ?? ''}`
|
|
251
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The value rows a published node shows, from its history in id order:
|
|
275
|
+
* invariant values as of its latest publish, each culture's as of its own.
|
|
276
|
+
* A culture that is not published contributes nothing. Pure.
|
|
277
|
+
*/
|
|
278
|
+
export function publishedValueRows<
|
|
279
|
+
R extends {
|
|
280
|
+
eventId: number
|
|
281
|
+
propertyTypeId: number
|
|
282
|
+
languageId: number | null
|
|
283
|
+
segment: string | null
|
|
284
|
+
},
|
|
285
|
+
>(
|
|
286
|
+
history: readonly R[],
|
|
287
|
+
invariantEventId: number,
|
|
288
|
+
cultures: ReadonlyArray<{ languageId: number; eventId: number }>,
|
|
289
|
+
): R[] {
|
|
290
|
+
const cutoff = new Map(cultures.map((c) => [c.languageId, c.eventId]))
|
|
291
|
+
const chosen = new Map<string, R>()
|
|
292
|
+
for (const row of history) {
|
|
293
|
+
const limit = row.languageId === null ? invariantEventId : cutoff.get(row.languageId)
|
|
294
|
+
if (limit === undefined || row.eventId > limit) continue
|
|
295
|
+
chosen.set(keyOf(row.propertyTypeId, row.languageId, row.segment), row)
|
|
296
|
+
}
|
|
297
|
+
return [...chosen.values()]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Ids in slices small enough for either dialect's parameter limit. */
|
|
301
|
+
const slices = <T>(items: readonly T[], size = 500): T[][] =>
|
|
302
|
+
Array.from({ length: Math.ceil(items.length / size) }, (_, n) =>
|
|
303
|
+
items.slice(n * size, (n + 1) * size),
|
|
304
|
+
)
|
|
305
|
+
|
|
252
306
|
export class DocumentRepository {
|
|
253
307
|
#db: Db
|
|
254
308
|
#nodes: NodeRepository
|
|
@@ -277,13 +331,13 @@ export class DocumentRepository {
|
|
|
277
331
|
}
|
|
278
332
|
|
|
279
333
|
/**
|
|
280
|
-
* The
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
334
|
+
* The tree's lock, unless the caller already holds it. Every write goes
|
|
335
|
+
* through here, as every Umbraco content service write takes its tree's write
|
|
336
|
+
* lock: two saves, or a save and a move, never interleave. The node a write
|
|
337
|
+
* acts on is read inside, so it acts on where the node is now.
|
|
284
338
|
*/
|
|
285
339
|
#withTree<T>(fn: () => Promise<T>): Promise<T> {
|
|
286
|
-
return this.#inTreeLock ? fn() : this.#db.locks.withLock(
|
|
340
|
+
return this.#inTreeLock ? fn() : this.#db.locks.withLock(KINDS[this.kind].lock, fn)
|
|
287
341
|
}
|
|
288
342
|
|
|
289
343
|
#child(tx: Db): DocumentRepository {
|
|
@@ -334,7 +388,11 @@ export class DocumentRepository {
|
|
|
334
388
|
? (await this.#nodes.byKey(options.parentKey))?.id
|
|
335
389
|
: undefined
|
|
336
390
|
if (options.parentKey && parentId === undefined) return { total: 0, items: [] }
|
|
337
|
-
return this.#nodes.search(this.objectType, query, {
|
|
391
|
+
return this.#nodes.search(this.objectType, query, {
|
|
392
|
+
...options,
|
|
393
|
+
parentId,
|
|
394
|
+
fullText: this.kind === 'document' || this.kind === 'media',
|
|
395
|
+
})
|
|
338
396
|
}
|
|
339
397
|
|
|
340
398
|
async versions(key: string): Promise<DocumentVersionSummary[]> {
|
|
@@ -387,20 +445,34 @@ export class DocumentRepository {
|
|
|
387
445
|
[t, t, f, this.objectType],
|
|
388
446
|
)
|
|
389
447
|
const languages = await this.#languages()
|
|
448
|
+
// Read in slices rather than per node: a large site is tens of thousands
|
|
449
|
+
// of nodes, and a query each is most of a rebuild's time.
|
|
450
|
+
const typesById = new Map<number, PropertyTypeRow[]>()
|
|
451
|
+
const culturesById = new Map<number, PublishedCulture[]>()
|
|
452
|
+
const historyById = new Map<number, ValueRow[]>()
|
|
453
|
+
for (const slice of slices(rows.map((row) => Number(row.id)))) {
|
|
454
|
+
for (const [id, cultures] of await this.#publishedCultureEventsFor(slice))
|
|
455
|
+
culturesById.set(id, cultures)
|
|
456
|
+
for (const [id, history] of await this.#valueHistoryFor(slice, asOfSchemaStateId))
|
|
457
|
+
historyById.set(id, history)
|
|
458
|
+
}
|
|
390
459
|
const published: PublishedNodeRow[] = []
|
|
391
460
|
for (const row of rows) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
461
|
+
const typeId = Number(row.content_type_id)
|
|
462
|
+
if (!typesById.has(typeId))
|
|
463
|
+
// A pending property is never rendered, even if a template asks for it.
|
|
464
|
+
typesById.set(
|
|
465
|
+
typeId,
|
|
466
|
+
(await this.#propertyTypes(typeId)).filter((p) => !p.pending),
|
|
467
|
+
)
|
|
468
|
+
const propertyTypes = typesById.get(typeId) as PropertyTypeRow[]
|
|
469
|
+
const cultures = culturesById.get(Number(row.id)) ?? []
|
|
470
|
+
const latest = Math.max(Number(row.event_id), ...cultures.map((c) => c.eventId))
|
|
471
|
+
const history = (historyById.get(Number(row.id)) ?? []).filter((v) => v.eventId <= latest)
|
|
472
|
+
const values = this.#valuesFromRows(
|
|
473
|
+
publishedValueRows(history, Number(row.event_id), cultures),
|
|
399
474
|
propertyTypes,
|
|
400
475
|
languages,
|
|
401
|
-
Number(row.event_id),
|
|
402
|
-
cultures,
|
|
403
|
-
asOfSchemaStateId,
|
|
404
476
|
)
|
|
405
477
|
published.push({
|
|
406
478
|
id: Number(row.id),
|
|
@@ -422,6 +494,87 @@ export class DocumentRepository {
|
|
|
422
494
|
return published
|
|
423
495
|
}
|
|
424
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Every node of this kind that is not in the bin, with the values an editor
|
|
499
|
+
* sees now: what the published cache holds for media, which has no publish.
|
|
500
|
+
* Read in slices, like `loadPublished`.
|
|
501
|
+
*/
|
|
502
|
+
async loadCurrent(): Promise<
|
|
503
|
+
Array<{
|
|
504
|
+
id: number
|
|
505
|
+
key: string
|
|
506
|
+
name: string
|
|
507
|
+
contentTypeAlias: string
|
|
508
|
+
values: DocumentValue[]
|
|
509
|
+
parentId: number
|
|
510
|
+
sortOrder: number
|
|
511
|
+
createDate: Date
|
|
512
|
+
updateDate: Date
|
|
513
|
+
}>
|
|
514
|
+
> {
|
|
515
|
+
const t = this.#db.dialect.boolValue(true)
|
|
516
|
+
const rows = await this.#db.query(
|
|
517
|
+
`SELECT n.id, n.unique_id, n.text, n.parent_id, n.sort_order, n.create_date,
|
|
518
|
+
head.text AS draft_name, head.version_date,
|
|
519
|
+
ct.alias AS content_type_alias, c.content_type_id
|
|
520
|
+
FROM node n
|
|
521
|
+
JOIN content c ON c.node_id = n.id
|
|
522
|
+
JOIN content_type ct ON ct.node_id = c.content_type_id
|
|
523
|
+
JOIN content_version head ON head.node_id = n.id AND head.current = ?
|
|
524
|
+
WHERE n.node_object_type = ? AND n.trashed = ?
|
|
525
|
+
ORDER BY n.level, n.sort_order, n.id`,
|
|
526
|
+
[t, this.objectType, this.#db.dialect.boolValue(false)],
|
|
527
|
+
)
|
|
528
|
+
const languages = await this.#languages()
|
|
529
|
+
const stateId = await visibleStateIdFor(this.#db, this.#nodeState)
|
|
530
|
+
const typesById = new Map<number, PropertyTypeRow[]>()
|
|
531
|
+
const rowsById = new Map<number, ValueRow[]>()
|
|
532
|
+
for (const slice of slices(rows.map((row) => Number(row.id)))) {
|
|
533
|
+
if (stateId === undefined) {
|
|
534
|
+
const current = await this.#db.query(
|
|
535
|
+
`SELECT id, node_id, event_id, property_type_id, language_id, segment,
|
|
536
|
+
int_value, decimal_value, date_value, varchar_value, text_value
|
|
537
|
+
FROM property_value WHERE node_id IN (${slice.map(() => '?').join(', ')}) AND is_current = ?`,
|
|
538
|
+
[...slice, t],
|
|
539
|
+
)
|
|
540
|
+
for (const row of current) {
|
|
541
|
+
const id = Number(row.node_id)
|
|
542
|
+
rowsById.set(id, [...(rowsById.get(id) ?? []), this.#toValueRow(row)])
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
// As of the state this node reads: the latest value per key at or before it.
|
|
546
|
+
for (const [id, history] of await this.#valueHistoryFor(slice, stateId)) {
|
|
547
|
+
const latest = new Map<string, ValueRow>()
|
|
548
|
+
for (const row of history)
|
|
549
|
+
latest.set(keyOf(row.propertyTypeId, row.languageId, row.segment), row)
|
|
550
|
+
rowsById.set(id, [...latest.values()])
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const out = []
|
|
555
|
+
for (const row of rows) {
|
|
556
|
+
const typeId = Number(row.content_type_id)
|
|
557
|
+
if (!typesById.has(typeId)) typesById.set(typeId, await this.#propertyTypes(typeId))
|
|
558
|
+
const propertyTypes = typesById.get(typeId) as PropertyTypeRow[]
|
|
559
|
+
out.push({
|
|
560
|
+
id: Number(row.id),
|
|
561
|
+
key: normaliseUuid(String(row.unique_id)),
|
|
562
|
+
name: String(row.draft_name ?? row.text ?? ''),
|
|
563
|
+
contentTypeAlias: String(row.content_type_alias),
|
|
564
|
+
parentId: Number(row.parent_id),
|
|
565
|
+
sortOrder: Number(row.sort_order),
|
|
566
|
+
createDate: DbDate.fromDb(row.create_date) ?? new Date(0),
|
|
567
|
+
updateDate:
|
|
568
|
+
DbDate.fromDb(row.version_date) ?? DbDate.fromDb(row.create_date) ?? new Date(0),
|
|
569
|
+
values:
|
|
570
|
+
propertyTypes.length === 0
|
|
571
|
+
? []
|
|
572
|
+
: this.#valuesFromRows(rowsById.get(Number(row.id)) ?? [], propertyTypes, languages),
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
return out
|
|
576
|
+
}
|
|
577
|
+
|
|
425
578
|
// --------------------------------------------------------------- writes
|
|
426
579
|
|
|
427
580
|
/** Creates a document with its first save event. Under the content-tree lock so sibling sort orders cannot race. */
|
|
@@ -433,9 +586,11 @@ export class DocumentRepository {
|
|
|
433
586
|
const contentTypeNode = await repo.nodes.byKey(input.contentTypeKey)
|
|
434
587
|
if (!contentTypeNode) throw new Error(`Unknown content type '${input.contentTypeKey}'.`)
|
|
435
588
|
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
589
|
+
const parent = input.parentKey ? await repo.nodes.byKey(input.parentKey) : undefined
|
|
590
|
+
if (input.parentKey && !parent)
|
|
591
|
+
throw new InvalidParentError('The parent could not be found.')
|
|
592
|
+
if (parent?.trashed) throw new InvalidParentError('The parent is in the recycle bin.')
|
|
593
|
+
const parentId = parent?.id ?? SystemNodes.Root
|
|
439
594
|
const name = input.variants[0]?.name ?? ''
|
|
440
595
|
const node = await repo.nodes.create({
|
|
441
596
|
key: input.key,
|
|
@@ -551,6 +706,14 @@ export class DocumentRepository {
|
|
|
551
706
|
key: string,
|
|
552
707
|
values: readonly MigratedValue[],
|
|
553
708
|
options: { schemaStateId: number; userId?: number },
|
|
709
|
+
): Promise<boolean> {
|
|
710
|
+
return this.#withTree(() => this.#appendUnderState(key, values, options))
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async #appendUnderState(
|
|
714
|
+
key: string,
|
|
715
|
+
values: readonly MigratedValue[],
|
|
716
|
+
options: { schemaStateId: number; userId?: number },
|
|
554
717
|
): Promise<boolean> {
|
|
555
718
|
return this.#db.transaction(async (tx) => {
|
|
556
719
|
const repo = this.#child(tx)
|
|
@@ -594,13 +757,20 @@ export class DocumentRepository {
|
|
|
594
757
|
)
|
|
595
758
|
written = true
|
|
596
759
|
}
|
|
597
|
-
if (written)
|
|
760
|
+
if (written) {
|
|
761
|
+
await repo.#recomputeEdited(node.id)
|
|
762
|
+
await queueForSearch(tx, [node.id])
|
|
763
|
+
}
|
|
598
764
|
return written
|
|
599
765
|
})
|
|
600
766
|
}
|
|
601
767
|
|
|
602
768
|
/** Saves the draft: one save event, and a new value version for each value that changed. */
|
|
603
769
|
async update(input: SaveDocumentInput): Promise<DocumentAggregate | undefined> {
|
|
770
|
+
return this.#withTree(() => this.#update(input))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async #update(input: SaveDocumentInput): Promise<DocumentAggregate | undefined> {
|
|
604
774
|
return this.#db.transaction(async (tx) => {
|
|
605
775
|
const repo = this.#child(tx)
|
|
606
776
|
const state = await assertNodeMayWrite(tx, this.#nodeState)
|
|
@@ -647,6 +817,9 @@ export class DocumentRepository {
|
|
|
647
817
|
await assertNodeMayWrite(tx, this.#nodeState)
|
|
648
818
|
const node = await repo.nodes.byKey(key)
|
|
649
819
|
if (!node) return undefined
|
|
820
|
+
// The bin is not a document, so the ancestor check below passes for a
|
|
821
|
+
// node trashed directly; Umbraco refuses it as FailedPublishIsTrashed.
|
|
822
|
+
if (node.trashed) throw new PublishBlockedError('The document is in the recycle bin.')
|
|
650
823
|
// An unpublished ancestor makes a published descendant unreachable, so
|
|
651
824
|
// Umbraco refuses the publish rather than creating a dead route.
|
|
652
825
|
if (!(await repo.#ancestorsPublished(node)))
|
|
@@ -723,6 +896,7 @@ export class DocumentRepository {
|
|
|
723
896
|
): Promise<string[]> {
|
|
724
897
|
const node = await this.#nodes.byKey(key)
|
|
725
898
|
if (!node) return ['there is no such document here']
|
|
899
|
+
if (node.trashed) return ['it is in the recycle bin']
|
|
726
900
|
const reasons: string[] = []
|
|
727
901
|
if (!(await this.#ancestorsPublished(node, alsoPublishing)))
|
|
728
902
|
reasons.push('an ancestor is not published')
|
|
@@ -760,6 +934,13 @@ export class DocumentRepository {
|
|
|
760
934
|
async unpublish(
|
|
761
935
|
key: string,
|
|
762
936
|
cultures: readonly string[] | null,
|
|
937
|
+
): Promise<DocumentAggregate | undefined> {
|
|
938
|
+
return this.#withTree(() => this.#unpublish(key, cultures))
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async #unpublish(
|
|
942
|
+
key: string,
|
|
943
|
+
cultures: readonly string[] | null,
|
|
763
944
|
): Promise<DocumentAggregate | undefined> {
|
|
764
945
|
return this.#db.transaction(async (tx) => {
|
|
765
946
|
const repo = this.#child(tx)
|
|
@@ -809,6 +990,10 @@ export class DocumentRepository {
|
|
|
809
990
|
|
|
810
991
|
/** Rollback appends the target version's values under a new event. History is never rewritten. */
|
|
811
992
|
async rollback(versionId: string): Promise<DocumentAggregate | undefined> {
|
|
993
|
+
return this.#withTree(() => this.#rollback(versionId))
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async #rollback(versionId: string): Promise<DocumentAggregate | undefined> {
|
|
812
997
|
return this.#db.transaction(async (tx) => {
|
|
813
998
|
const repo = this.#child(tx)
|
|
814
999
|
const state = await assertNodeMayWrite(tx, this.#nodeState)
|
|
@@ -861,35 +1046,54 @@ export class DocumentRepository {
|
|
|
861
1046
|
return true
|
|
862
1047
|
}
|
|
863
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* Re-parents the branch under the bin, as Umbraco does: paths stay true, so a
|
|
1051
|
+
* later restore of an ancestor cannot reach into it. The branch root
|
|
1052
|
+
* remembers where it came from for the restore.
|
|
1053
|
+
*/
|
|
864
1054
|
async moveToRecycleBin(key: string): Promise<boolean> {
|
|
865
|
-
const
|
|
866
|
-
if (
|
|
867
|
-
return this.#withTree(
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
node.parentId ===
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1055
|
+
const bin = this.#recycleBin
|
|
1056
|
+
if (bin === undefined) return false
|
|
1057
|
+
return this.#withTree(() =>
|
|
1058
|
+
this.#db.transaction(async (tx) => {
|
|
1059
|
+
const repo = this.#child(tx)
|
|
1060
|
+
const node = await repo.nodes.byKey(key)
|
|
1061
|
+
if (!node) return false
|
|
1062
|
+
if (node.parentId === bin) return true
|
|
1063
|
+
const t = tx.dialect.boolValue(true)
|
|
1064
|
+
const f = tx.dialect.boolValue(false)
|
|
1065
|
+
await tx.exec('UPDATE document SET original_parent_id = ? WHERE node_id = ?', [
|
|
1066
|
+
node.parentId === SystemNodes.Root ? null : node.parentId,
|
|
1067
|
+
node.id,
|
|
1068
|
+
])
|
|
1069
|
+
// The whole branch goes with it, and stays unpublished while trashed.
|
|
1070
|
+
await tx.exec('UPDATE node SET trashed = ? WHERE id = ? OR path LIKE ?', [
|
|
1071
|
+
t,
|
|
1072
|
+
node.id,
|
|
1073
|
+
`${node.path},%`,
|
|
1074
|
+
])
|
|
1075
|
+
await tx.exec(
|
|
1076
|
+
`UPDATE document SET published = ?, edited = ? WHERE node_id IN (SELECT id FROM node WHERE id = ? OR path LIKE ?)`,
|
|
1077
|
+
[f, t, node.id, `${node.path},%`],
|
|
1078
|
+
)
|
|
1079
|
+
await repo.nodes.move(node.id, bin)
|
|
1080
|
+
await tx.exec('UPDATE node SET sort_order = ? WHERE id = ?', [
|
|
1081
|
+
await repo.#nextSortOrder(bin, node.id),
|
|
1082
|
+
node.id,
|
|
1083
|
+
])
|
|
1084
|
+
return true
|
|
1085
|
+
}),
|
|
1086
|
+
)
|
|
887
1087
|
}
|
|
888
1088
|
|
|
889
1089
|
async delete(key: string): Promise<boolean> {
|
|
890
|
-
|
|
891
|
-
|
|
1090
|
+
return this.#withTree(() => this.#delete(key))
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async #delete(key: string): Promise<boolean> {
|
|
892
1094
|
return this.#db.transaction(async (tx) => {
|
|
1095
|
+
const node = await new NodeRepository(tx).byKey(key)
|
|
1096
|
+
if (!node) return false
|
|
893
1097
|
const ids = await tx.query<{ id: number }>(
|
|
894
1098
|
'SELECT id FROM node WHERE id = ? OR path LIKE ?',
|
|
895
1099
|
[node.id, `${node.path},%`],
|
|
@@ -939,6 +1143,17 @@ export class DocumentRepository {
|
|
|
939
1143
|
return 'invalid'
|
|
940
1144
|
if (target?.trashed) return 'invalid'
|
|
941
1145
|
const parentId = target?.id ?? SystemNodes.Root
|
|
1146
|
+
// Out of the bin is a restore, as Umbraco's Move treats it.
|
|
1147
|
+
if (node.trashed) {
|
|
1148
|
+
await tx.exec('UPDATE node SET trashed = ? WHERE id = ? OR path LIKE ?', [
|
|
1149
|
+
tx.dialect.boolValue(false),
|
|
1150
|
+
node.id,
|
|
1151
|
+
`${node.path},%`,
|
|
1152
|
+
])
|
|
1153
|
+
await tx.exec('UPDATE document SET original_parent_id = NULL WHERE node_id = ?', [
|
|
1154
|
+
node.id,
|
|
1155
|
+
])
|
|
1156
|
+
}
|
|
942
1157
|
await repo.nodes.move(node.id, parentId)
|
|
943
1158
|
await tx.exec('UPDATE node SET sort_order = ? WHERE id = ?', [
|
|
944
1159
|
await repo.#nextSortOrder(parentId, node.id),
|
|
@@ -1002,11 +1217,13 @@ export class DocumentRepository {
|
|
|
1002
1217
|
parentKey: string | null,
|
|
1003
1218
|
sorting: ReadonlyArray<{ key: string; sortOrder: number }>,
|
|
1004
1219
|
): Promise<'sorted' | 'notFound'> {
|
|
1005
|
-
const parentId = parentKey ? (await this.#nodes.byKey(parentKey))?.id : SystemNodes.Root
|
|
1006
|
-
if (parentId === undefined) return 'notFound'
|
|
1007
1220
|
return this.#withTree(async () =>
|
|
1008
1221
|
this.#db.transaction(async (tx) => {
|
|
1009
1222
|
await assertNodeMayWrite(tx, this.#nodeState)
|
|
1223
|
+
const parentId = parentKey
|
|
1224
|
+
? (await new NodeRepository(tx).byKey(parentKey))?.id
|
|
1225
|
+
: SystemNodes.Root
|
|
1226
|
+
if (parentId === undefined) return 'notFound'
|
|
1010
1227
|
const nodes = await new NodeRepository(tx).byKeys(sorting.map((s) => s.key))
|
|
1011
1228
|
const byKey = new Map(nodes.map((n) => [n.key, n]))
|
|
1012
1229
|
for (const item of sorting) {
|
|
@@ -1061,17 +1278,17 @@ export class DocumentRepository {
|
|
|
1061
1278
|
key: string,
|
|
1062
1279
|
targetKey: string | null | undefined,
|
|
1063
1280
|
): Promise<'restored' | 'notFound' | 'invalid'> {
|
|
1064
|
-
const node = await this.#nodes.byKey(key)
|
|
1065
|
-
if (!node || node.objectType !== this.objectType || !node.trashed) return 'notFound'
|
|
1066
|
-
let parentKey: string | null
|
|
1067
|
-
if (targetKey !== undefined && targetKey !== null) parentKey = targetKey
|
|
1068
|
-
else if (targetKey === null) parentKey = null
|
|
1069
|
-
else parentKey = (await this.originalParent(key)) ?? null
|
|
1070
|
-
const parent = parentKey ? await this.#nodes.byKey(parentKey) : undefined
|
|
1071
|
-
if (parentKey && (!parent || parent.trashed)) return 'invalid'
|
|
1072
1281
|
return this.#withTree(async () =>
|
|
1073
1282
|
this.#db.transaction(async (tx) => {
|
|
1074
1283
|
const repo = this.#child(tx)
|
|
1284
|
+
const node = await repo.nodes.byKey(key)
|
|
1285
|
+
if (!node || node.objectType !== this.objectType || !node.trashed) return 'notFound'
|
|
1286
|
+
let parentKey: string | null
|
|
1287
|
+
if (targetKey !== undefined && targetKey !== null) parentKey = targetKey
|
|
1288
|
+
else if (targetKey === null) parentKey = null
|
|
1289
|
+
else parentKey = (await repo.originalParent(key)) ?? null
|
|
1290
|
+
const parent = parentKey ? await repo.nodes.byKey(parentKey) : undefined
|
|
1291
|
+
if (parentKey && (!parent || parent.trashed)) return 'invalid'
|
|
1075
1292
|
await assertNodeMayWrite(tx, this.#nodeState)
|
|
1076
1293
|
const parentId = parent?.id ?? SystemNodes.Root
|
|
1077
1294
|
await tx.exec('UPDATE node SET trashed = ? WHERE id = ? OR path LIKE ?', [
|
|
@@ -1100,6 +1317,14 @@ export class DocumentRepository {
|
|
|
1100
1317
|
key: string,
|
|
1101
1318
|
cultures: readonly string[] | null,
|
|
1102
1319
|
includeUnpublished: boolean,
|
|
1320
|
+
): Promise<{ published: string[]; failed: Array<{ key: string; reason: string }> } | undefined> {
|
|
1321
|
+
return this.#withTree(() => this.#publishBranch(key, cultures, includeUnpublished))
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async #publishBranch(
|
|
1325
|
+
key: string,
|
|
1326
|
+
cultures: readonly string[] | null,
|
|
1327
|
+
includeUnpublished: boolean,
|
|
1103
1328
|
): Promise<{ published: string[]; failed: Array<{ key: string; reason: string }> } | undefined> {
|
|
1104
1329
|
const root = await this.#nodes.byKey(key)
|
|
1105
1330
|
if (!root || root.objectType !== this.objectType || root.trashed) return undefined
|
|
@@ -1182,6 +1407,17 @@ export class DocumentRepository {
|
|
|
1182
1407
|
publishTime: Date | null
|
|
1183
1408
|
unpublishTime: Date | null
|
|
1184
1409
|
}>,
|
|
1410
|
+
): Promise<boolean> {
|
|
1411
|
+
return this.#withTree(() => this.#setSchedule(key, entries))
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async #setSchedule(
|
|
1415
|
+
key: string,
|
|
1416
|
+
entries: ReadonlyArray<{
|
|
1417
|
+
culture: string | null
|
|
1418
|
+
publishTime: Date | null
|
|
1419
|
+
unpublishTime: Date | null
|
|
1420
|
+
}>,
|
|
1185
1421
|
): Promise<boolean> {
|
|
1186
1422
|
const node = await this.#nodes.byKey(key)
|
|
1187
1423
|
if (!node || node.objectType !== this.objectType) return false
|
|
@@ -1289,11 +1525,13 @@ export class DocumentRepository {
|
|
|
1289
1525
|
type.keep_all === null ? global.keepAllVersionsNewerThanDays : Number(type.keep_all)
|
|
1290
1526
|
const keepLatest =
|
|
1291
1527
|
type.keep_latest === null ? global.keepLatestVersionPerDayForDays : Number(type.keep_latest)
|
|
1292
|
-
const deleted = await this.#
|
|
1293
|
-
this.#
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1528
|
+
const deleted = await this.#withTree(() =>
|
|
1529
|
+
this.#db.transaction((tx) =>
|
|
1530
|
+
this.#child(tx).#cleanupNode(
|
|
1531
|
+
Number(type.node_id),
|
|
1532
|
+
now.getTime() - keepAll * day,
|
|
1533
|
+
now.getTime() - keepLatest * day,
|
|
1534
|
+
),
|
|
1297
1535
|
),
|
|
1298
1536
|
)
|
|
1299
1537
|
if (deleted > 0) {
|
|
@@ -1479,6 +1717,15 @@ export class DocumentRepository {
|
|
|
1479
1717
|
}))
|
|
1480
1718
|
}
|
|
1481
1719
|
|
|
1720
|
+
/** The property aliases a document's type has, compositions included; undefined when there is no document. */
|
|
1721
|
+
async propertyAliases(key: string): Promise<string[] | undefined> {
|
|
1722
|
+
const node = await this.#nodes.byKey(key)
|
|
1723
|
+
if (!node) return undefined
|
|
1724
|
+
const contentTypeNodeId = await this.#contentTypeNodeId(node.id)
|
|
1725
|
+
if (!contentTypeNodeId) return undefined
|
|
1726
|
+
return (await this.#propertyTypes(contentTypeNodeId)).map((p) => p.alias)
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1482
1729
|
/** Property types of a content type, including everything it composes; retired ones excluded. */
|
|
1483
1730
|
async #propertyTypes(contentTypeNodeId: number): Promise<PropertyTypeRow[]> {
|
|
1484
1731
|
const rows = await this.#db.query(
|
|
@@ -1979,33 +2226,90 @@ export class DocumentRepository {
|
|
|
1979
2226
|
return { ...draft, values }
|
|
1980
2227
|
}
|
|
1981
2228
|
|
|
1982
|
-
/**
|
|
2229
|
+
/**
|
|
2230
|
+
* The content tree's view of documents: four queries a page, whatever its
|
|
2231
|
+
* size. Only what a tree item shows is read — not the values, nor the
|
|
2232
|
+
* schedules the editor needs.
|
|
2233
|
+
*/
|
|
1983
2234
|
async treeItems(rows: readonly NodeRow[]): Promise<DocumentTreeItem[]> {
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2235
|
+
if (rows.length === 0) return []
|
|
2236
|
+
const ids = rows.map((row) => row.id)
|
|
2237
|
+
const placeholders = ids.map(() => '?').join(', ')
|
|
2238
|
+
const counts = await this.#nodes.childCounts(ids, this.objectType)
|
|
2239
|
+
const facts = await this.#db.query(
|
|
2240
|
+
`SELECT n.id, ctn.unique_id AS content_type_key, ct.icon AS content_type_icon,
|
|
2241
|
+
ct.list_view AS content_type_collection, d.published, d.edited,
|
|
2242
|
+
head.text AS draft_name
|
|
2243
|
+
FROM node n
|
|
2244
|
+
JOIN content c ON c.node_id = n.id
|
|
2245
|
+
JOIN content_type ct ON ct.node_id = c.content_type_id
|
|
2246
|
+
JOIN node ctn ON ctn.id = ct.node_id
|
|
2247
|
+
JOIN document d ON d.node_id = n.id
|
|
2248
|
+
JOIN content_version head ON head.node_id = n.id AND head.current = ?
|
|
2249
|
+
WHERE n.id IN (${placeholders})`,
|
|
2250
|
+
[this.#db.dialect.boolValue(true), ...ids],
|
|
1987
2251
|
)
|
|
2252
|
+
const factsById = new Map(facts.map((row) => [Number(row.id), row]))
|
|
2253
|
+
const cultures = await this.#db.query(
|
|
2254
|
+
`SELECT dcv.node_id, dcv.name, dcv.published, dcv.edited, dcv.available, l.iso_code
|
|
2255
|
+
FROM document_culture_variation dcv JOIN language l ON l.id = dcv.language_id
|
|
2256
|
+
WHERE dcv.node_id IN (${placeholders})`,
|
|
2257
|
+
ids,
|
|
2258
|
+
)
|
|
2259
|
+
const culturesById = new Map<number, Record<string, unknown>[]>()
|
|
2260
|
+
for (const row of cultures)
|
|
2261
|
+
culturesById.set(Number(row.node_id), [...(culturesById.get(Number(row.node_id)) ?? []), row])
|
|
2262
|
+
// Every ancestor the page's paths name, parents included, in one read.
|
|
2263
|
+
const pathIds = (row: NodeRow) =>
|
|
2264
|
+
row.path
|
|
2265
|
+
.split(',')
|
|
2266
|
+
.map(Number)
|
|
2267
|
+
.filter((id) => id > 0 && id !== row.id)
|
|
2268
|
+
const above = new Map(
|
|
2269
|
+
(await this.#nodes.byIds([...new Set(rows.flatMap(pathIds))])).map((n) => [n.id, n]),
|
|
2270
|
+
)
|
|
2271
|
+
|
|
1988
2272
|
const items: DocumentTreeItem[] = []
|
|
1989
2273
|
for (const row of rows) {
|
|
1990
|
-
const
|
|
1991
|
-
if (!
|
|
1992
|
-
const ancestors =
|
|
2274
|
+
const fact = factsById.get(row.id)
|
|
2275
|
+
if (!fact) continue
|
|
2276
|
+
const ancestors = pathIds(row)
|
|
2277
|
+
.map((id) => above.get(id))
|
|
2278
|
+
.filter((n): n is NodeRow => n !== undefined)
|
|
2279
|
+
const parent = row.parentId > 0 ? above.get(row.parentId) : undefined
|
|
2280
|
+
const own = culturesById.get(row.id) ?? []
|
|
1993
2281
|
items.push({
|
|
1994
2282
|
key: row.key,
|
|
1995
2283
|
name: row.text ?? '',
|
|
1996
2284
|
hasChildren: (counts.get(row.id) ?? 0) > 0,
|
|
1997
|
-
parentKey:
|
|
1998
|
-
contentTypeKey:
|
|
1999
|
-
contentTypeCollectionKey:
|
|
2000
|
-
|
|
2285
|
+
parentKey: parent ? parent.key : null,
|
|
2286
|
+
contentTypeKey: normaliseUuid(String(fact.content_type_key)),
|
|
2287
|
+
contentTypeCollectionKey: fact.content_type_collection
|
|
2288
|
+
? normaliseUuid(String(fact.content_type_collection))
|
|
2289
|
+
: null,
|
|
2290
|
+
icon: String(fact.content_type_icon ?? 'icon-document'),
|
|
2001
2291
|
isTrashed: row.trashed,
|
|
2002
2292
|
createDate: row.createDate ?? new Date(),
|
|
2003
2293
|
ancestorKeys: ancestors.filter((a) => a.objectType === this.objectType).map((a) => a.key),
|
|
2004
|
-
variants:
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2294
|
+
variants:
|
|
2295
|
+
own.length === 0
|
|
2296
|
+
? [
|
|
2297
|
+
{
|
|
2298
|
+
culture: null,
|
|
2299
|
+
name: String(fact.draft_name ?? row.text ?? ''),
|
|
2300
|
+
state: stateFor(fromDbBool(fact.published), fromDbBool(fact.edited), row.trashed),
|
|
2301
|
+
},
|
|
2302
|
+
]
|
|
2303
|
+
: own.map((c) => ({
|
|
2304
|
+
culture: String(c.iso_code),
|
|
2305
|
+
name: String(c.name ?? ''),
|
|
2306
|
+
state: stateFor(
|
|
2307
|
+
fromDbBool(c.published),
|
|
2308
|
+
fromDbBool(c.edited),
|
|
2309
|
+
row.trashed,
|
|
2310
|
+
fromDbBool(c.available),
|
|
2311
|
+
),
|
|
2312
|
+
})),
|
|
2009
2313
|
})
|
|
2010
2314
|
}
|
|
2011
2315
|
return items
|
|
@@ -2053,6 +2357,8 @@ export class DocumentRepository {
|
|
|
2053
2357
|
'INSERT INTO document_version (id, template_id, published) VALUES (?, ?, ?)',
|
|
2054
2358
|
[eventId, templateId, this.#db.dialect.boolValue(published)],
|
|
2055
2359
|
)
|
|
2360
|
+
// Every change to what an item says is an event, so this is where the search index hears of it.
|
|
2361
|
+
await queueForSearch(this.#db, [nodeId])
|
|
2056
2362
|
return eventId
|
|
2057
2363
|
}
|
|
2058
2364
|
|
|
@@ -2270,9 +2576,61 @@ export class DocumentRepository {
|
|
|
2270
2576
|
}
|
|
2271
2577
|
|
|
2272
2578
|
/** Each published culture: its language, the event it was published at, and its published name. */
|
|
2273
|
-
async #
|
|
2274
|
-
|
|
2275
|
-
): Promise<
|
|
2579
|
+
async #publishedCultureEventsFor(
|
|
2580
|
+
ids: readonly number[],
|
|
2581
|
+
): Promise<Map<number, PublishedCulture[]>> {
|
|
2582
|
+
const out = new Map<number, PublishedCulture[]>()
|
|
2583
|
+
if (ids.length === 0) return out
|
|
2584
|
+
const rows = await this.#db.query(
|
|
2585
|
+
`SELECT dcv.node_id, dcv.language_id, l.iso_code, dcv.published_event_id, dcv.published_name, dcv.name
|
|
2586
|
+
FROM document_culture_variation dcv JOIN language l ON l.id = dcv.language_id
|
|
2587
|
+
WHERE dcv.node_id IN (${ids.map(() => '?').join(', ')}) AND dcv.published = ?`,
|
|
2588
|
+
[...ids, this.#db.dialect.boolValue(true)],
|
|
2589
|
+
)
|
|
2590
|
+
for (const row of rows) {
|
|
2591
|
+
const id = Number(row.node_id)
|
|
2592
|
+
out.set(id, [
|
|
2593
|
+
...(out.get(id) ?? []),
|
|
2594
|
+
{
|
|
2595
|
+
languageId: Number(row.language_id),
|
|
2596
|
+
isoCode: String(row.iso_code),
|
|
2597
|
+
eventId: Number(row.published_event_id ?? 0),
|
|
2598
|
+
name: String(row.published_name ?? row.name ?? ''),
|
|
2599
|
+
},
|
|
2600
|
+
])
|
|
2601
|
+
}
|
|
2602
|
+
return out
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/** Each node's value history, oldest first. */
|
|
2606
|
+
async #valueHistoryFor(
|
|
2607
|
+
ids: readonly number[],
|
|
2608
|
+
schemaStateId?: number,
|
|
2609
|
+
): Promise<Map<number, ValueRow[]>> {
|
|
2610
|
+
const out = new Map<number, ValueRow[]>()
|
|
2611
|
+
if (ids.length === 0) return out
|
|
2612
|
+
const params: unknown[] = [...ids]
|
|
2613
|
+
let where = `node_id IN (${ids.map(() => '?').join(', ')})`
|
|
2614
|
+
if (schemaStateId !== undefined) {
|
|
2615
|
+
where += ' AND schema_state_id <= ?'
|
|
2616
|
+
params.push(schemaStateId)
|
|
2617
|
+
}
|
|
2618
|
+
const rows = await this.#db.query(
|
|
2619
|
+
`SELECT id, node_id, event_id, property_type_id, language_id, segment,
|
|
2620
|
+
int_value, decimal_value, date_value, varchar_value, text_value
|
|
2621
|
+
FROM property_value WHERE ${where} ORDER BY node_id, id`,
|
|
2622
|
+
params,
|
|
2623
|
+
)
|
|
2624
|
+
for (const row of rows) {
|
|
2625
|
+
const id = Number(row.node_id)
|
|
2626
|
+
const list = out.get(id)
|
|
2627
|
+
if (list) list.push(this.#toValueRow(row))
|
|
2628
|
+
else out.set(id, [this.#toValueRow(row)])
|
|
2629
|
+
}
|
|
2630
|
+
return out
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
async #publishedCultureEvents(nodeId: number): Promise<PublishedCulture[]> {
|
|
2276
2634
|
const rows = await this.#db.query(
|
|
2277
2635
|
`SELECT dcv.language_id, l.iso_code, dcv.published_event_id, dcv.published_name, dcv.name
|
|
2278
2636
|
FROM document_culture_variation dcv JOIN language l ON l.id = dcv.language_id
|
|
@@ -2307,7 +2665,6 @@ export class DocumentRepository {
|
|
|
2307
2665
|
cultures: ReadonlyArray<{ languageId: number; eventId: number }>,
|
|
2308
2666
|
schemaStateId?: number,
|
|
2309
2667
|
): Promise<DocumentValue[]> {
|
|
2310
|
-
const cutoff = new Map(cultures.map((c) => [c.languageId, c.eventId]))
|
|
2311
2668
|
const latest = Math.max(invariantEventId, ...cultures.map((c) => c.eventId))
|
|
2312
2669
|
const conditions = ['node_id = ?', 'event_id <= ?']
|
|
2313
2670
|
const params: unknown[] = [nodeId, latest]
|
|
@@ -2323,13 +2680,11 @@ export class DocumentRepository {
|
|
|
2323
2680
|
params,
|
|
2324
2681
|
)
|
|
2325
2682
|
).map((row) => this.#toValueRow(row))
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
}
|
|
2332
|
-
return this.#valuesFromRows([...chosen.values()], propertyTypes, languages)
|
|
2683
|
+
return this.#valuesFromRows(
|
|
2684
|
+
publishedValueRows(all, invariantEventId, cultures),
|
|
2685
|
+
propertyTypes,
|
|
2686
|
+
languages,
|
|
2687
|
+
)
|
|
2333
2688
|
}
|
|
2334
2689
|
|
|
2335
2690
|
async #anyCulturePublished(nodeId: number): Promise<boolean> {
|