@highstate/backend 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/{chunk-V2NILDHS.js → chunk-52MY2TCE.js} +347 -19
  2. package/dist/chunk-52MY2TCE.js.map +1 -0
  3. package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
  4. package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
  5. package/dist/highstate.manifest.json +4 -4
  6. package/dist/index.js +4159 -785
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +5 -2
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +2 -2
  11. package/package.json +7 -7
  12. package/prisma/backend/_schema/object.prisma +12 -0
  13. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  14. package/prisma/project/artifact.prisma +3 -0
  15. package/prisma/project/entity.prisma +125 -0
  16. package/prisma/project/instance.prisma +6 -0
  17. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  18. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  19. package/prisma/project/operation.prisma +3 -0
  20. package/src/business/artifact.test.ts +22 -2
  21. package/src/business/artifact.ts +7 -1
  22. package/src/business/entity-snapshot.test.ts +684 -0
  23. package/src/business/entity-snapshot.ts +904 -0
  24. package/src/business/evaluation.test.ts +56 -0
  25. package/src/business/evaluation.ts +102 -22
  26. package/src/business/global-search.test.ts +344 -0
  27. package/src/business/global-search.ts +902 -0
  28. package/src/business/index.ts +4 -0
  29. package/src/business/instance-lock.ts +58 -74
  30. package/src/business/instance-state.test.ts +15 -1
  31. package/src/business/instance-state.ts +37 -14
  32. package/src/business/object-ref-index.test.ts +140 -0
  33. package/src/business/object-ref-index.ts +193 -0
  34. package/src/business/operation.test.ts +15 -1
  35. package/src/business/operation.ts +4 -0
  36. package/src/business/project-model.ts +154 -13
  37. package/src/business/project-unlock.ts +25 -2
  38. package/src/business/project.ts +9 -0
  39. package/src/business/secret.test.ts +35 -2
  40. package/src/business/secret.ts +32 -9
  41. package/src/business/settings.ts +761 -0
  42. package/src/business/unit-output.test.ts +477 -0
  43. package/src/business/unit-output.ts +461 -0
  44. package/src/business/worker.ts +55 -4
  45. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  47. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  49. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  50. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  51. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  52. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  54. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  56. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  57. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  58. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  59. package/src/database/_generated/project/browser.ts +23 -0
  60. package/src/database/_generated/project/client.ts +23 -0
  61. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  62. package/src/database/_generated/project/enums.ts +8 -0
  63. package/src/database/_generated/project/internal/class.ts +53 -5
  64. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  65. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  66. package/src/database/_generated/project/models/Artifact.ts +199 -11
  67. package/src/database/_generated/project/models/Entity.ts +1274 -0
  68. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  69. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  70. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  71. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  72. package/src/database/_generated/project/models/Operation.ts +148 -3
  73. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  74. package/src/database/_generated/project/models.ts +4 -0
  75. package/src/database/migration.ts +3 -0
  76. package/src/library/worker/evaluator.ts +7 -1
  77. package/src/orchestrator/manager.ts +7 -0
  78. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  79. package/src/orchestrator/operation-context.ts +154 -16
  80. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  81. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  82. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  83. package/src/orchestrator/operation-plan.md +4 -1
  84. package/src/orchestrator/operation-plan.ts +286 -92
  85. package/src/orchestrator/operation-plan.update.test.md +286 -11
  86. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  87. package/src/orchestrator/operation-workset.ts +72 -22
  88. package/src/orchestrator/operation.cancel.test.ts +4 -0
  89. package/src/orchestrator/operation.composite.test.ts +341 -0
  90. package/src/orchestrator/operation.destroy.test.ts +4 -0
  91. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  92. package/src/orchestrator/operation.preview.test.ts +4 -0
  93. package/src/orchestrator/operation.refresh.test.ts +4 -0
  94. package/src/orchestrator/operation.test-utils.ts +52 -13
  95. package/src/orchestrator/operation.ts +228 -68
  96. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  97. package/src/orchestrator/operation.update.skip.test.ts +110 -0
  98. package/src/orchestrator/operation.update.test.ts +4 -0
  99. package/src/orchestrator/plan-test-builder.ts +1 -0
  100. package/src/orchestrator/unit-input-values.test.ts +450 -0
  101. package/src/orchestrator/unit-input-values.ts +281 -0
  102. package/src/pubsub/manager.ts +3 -0
  103. package/src/runner/abstractions.ts +23 -54
  104. package/src/runner/local.ts +109 -85
  105. package/src/services.ts +52 -1
  106. package/src/shared/models/prisma.ts +1 -0
  107. package/src/shared/models/project/entity.ts +121 -0
  108. package/src/shared/models/project/index.ts +1 -0
  109. package/src/shared/models/project/operation.ts +61 -3
  110. package/src/shared/models/project/state.ts +10 -0
  111. package/src/shared/models/project/worker.ts +7 -0
  112. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  113. package/src/shared/resolvers/effective-output-type.ts +162 -0
  114. package/src/shared/resolvers/index.ts +1 -0
  115. package/src/shared/resolvers/input.ts +59 -9
  116. package/src/shared/utils/index.ts +1 -0
  117. package/src/shared/utils/stable-json.ts +41 -0
  118. package/src/terminal/manager.ts +6 -0
  119. package/src/worker/manager.ts +97 -1
  120. package/dist/chunk-V2NILDHS.js.map +0 -1
@@ -3,6 +3,10 @@ import type { DatabaseManager } from "../database"
3
3
  import type {
4
4
  ApiKeyWhereInput,
5
5
  ArtifactWhereInput,
6
+ EntityOrderByWithRelationInput,
7
+ EntitySnapshotReferenceOrderByWithRelationInput,
8
+ EntitySnapshotReferenceWhereInput,
9
+ EntityWhereInput,
6
10
  OperationWhereInput,
7
11
  PageWhereInput,
8
12
  SecretWhereInput,
@@ -20,6 +24,12 @@ import type {
20
24
  ArtifactQuery,
21
25
  CollectionQuery,
22
26
  CollectionQueryResult,
27
+ EntityDetailsOutput,
28
+ EntityOutput,
29
+ EntityQuery,
30
+ EntityReferenceOutput,
31
+ EntitySnapshotDetailsOutput,
32
+ EntitySnapshotListItemOutput,
23
33
  OperationOutput,
24
34
  OperationType,
25
35
  PageDetailsOutput,
@@ -40,9 +50,17 @@ import type {
40
50
  WorkerQuery,
41
51
  WorkerVersionOutput,
42
52
  } from "../shared"
53
+ import { z } from "zod"
43
54
  import {
44
55
  apiKeyOutputSchema,
45
56
  artifactOutputSchema,
57
+ collectionQuerySchema,
58
+ entityDetailsOutputSchema,
59
+ entityOutputSchema,
60
+ entityQuerySchema,
61
+ entityReferenceOutputSchema,
62
+ entitySnapshotDetailsOutputSchema,
63
+ entitySnapshotListItemOutputSchema,
46
64
  forSchema,
47
65
  operationOutputSchema,
48
66
  pageDetailsOutputSchema,
@@ -52,6 +70,7 @@ import {
52
70
  terminalDetailsOutputSchema,
53
71
  terminalOutputSchema,
54
72
  toApiKeyOutput,
73
+ toCommonEntityMeta,
55
74
  toPageOutput,
56
75
  toSecretOutput,
57
76
  toTerminalDetailsOutput,
@@ -67,6 +86,745 @@ import {
67
86
  export class SettingsService {
68
87
  constructor(private readonly database: DatabaseManager) {}
69
88
 
89
+ private buildEntityWhere(query: EntityQuery): EntityWhereInput {
90
+ const where: EntityWhereInput = {}
91
+
92
+ if (query.type) {
93
+ where.type = { contains: query.type }
94
+ }
95
+
96
+ if (!query.search) return where
97
+
98
+ return {
99
+ ...where,
100
+ OR: [
101
+ { id: { contains: query.search } },
102
+ { type: { contains: query.search } },
103
+ { identity: { contains: query.search } },
104
+ ],
105
+ }
106
+ }
107
+
108
+ private buildEntityOrderBy(query: EntityQuery) {
109
+ const defaultOrderBy: EntityOrderByWithRelationInput[] = [{ type: "asc" }, { identity: "asc" }]
110
+
111
+ const sort = query.sortBy?.[0] ?? null
112
+ if (!sort) return defaultOrderBy
113
+
114
+ if (sort.key === "type") return [{ type: sort.order }]
115
+ if (sort.key === "identity") return [{ identity: sort.order }]
116
+
117
+ return defaultOrderBy
118
+ }
119
+
120
+ private buildEntitySnapshotWhere(
121
+ query: CollectionQuery,
122
+ snapshotIdField: "fromId" | "toId",
123
+ snapshotId: string,
124
+ ) {
125
+ const baseWhere = { [snapshotIdField]: snapshotId } as EntitySnapshotReferenceWhereInput
126
+
127
+ if (!query.search) return baseWhere
128
+
129
+ return {
130
+ AND: [
131
+ baseWhere,
132
+ {
133
+ OR: [
134
+ { group: { contains: query.search } },
135
+ { [snapshotIdField]: { contains: query.search } },
136
+ ],
137
+ },
138
+ ],
139
+ }
140
+ }
141
+
142
+ private buildEntitySnapshotOrderBy(
143
+ query: CollectionQuery,
144
+ ): EntitySnapshotReferenceOrderByWithRelationInput[] {
145
+ const defaultOrderBy: EntitySnapshotReferenceOrderByWithRelationInput[] = [
146
+ { group: "asc" },
147
+ { kind: "asc" },
148
+ ]
149
+
150
+ const sort = query.sortBy?.[0] ?? null
151
+ if (!sort) return defaultOrderBy
152
+
153
+ if (sort.key === "group") return [{ group: sort.order }, { kind: "asc" }]
154
+
155
+ return defaultOrderBy
156
+ }
157
+
158
+ async queryEntities(
159
+ projectId: string,
160
+ query: EntityQuery,
161
+ ): Promise<CollectionQueryResult<EntityOutput>> {
162
+ const parsedQuery = entityQuerySchema.parse(query)
163
+ const db = await this.database.forProject(projectId)
164
+ const whereClause = this.buildEntityWhere(parsedQuery)
165
+
166
+ const [total, items] = await Promise.all([
167
+ db.entity.count({ where: whereClause }),
168
+ db.entity.findMany({
169
+ where: whereClause,
170
+ orderBy: this.buildEntityOrderBy(parsedQuery),
171
+ skip: parsedQuery.skip,
172
+ take: parsedQuery.count,
173
+ select: {
174
+ id: true,
175
+ type: true,
176
+ identity: true,
177
+ snapshots: {
178
+ orderBy: { createdAt: "desc" },
179
+ take: 1,
180
+ select: {
181
+ id: true,
182
+ content: {
183
+ select: {
184
+ meta: true,
185
+ },
186
+ },
187
+ createdAt: true,
188
+ },
189
+ },
190
+ },
191
+ }),
192
+ ])
193
+
194
+ const outputItems = items.map(item => {
195
+ const lastSnapshot = item.snapshots[0] ?? null
196
+ const meta = toCommonEntityMeta(lastSnapshot?.content.meta)
197
+
198
+ const parsedOutput = entityOutputSchema.parse({
199
+ id: item.id,
200
+ type: item.type,
201
+ identity: item.identity,
202
+ meta: meta.title ? meta : { ...meta, title: item.identity },
203
+ snapshotId: lastSnapshot?.id,
204
+ createdAt: lastSnapshot?.createdAt,
205
+ })
206
+
207
+ return parsedOutput
208
+ })
209
+
210
+ return { items: outputItems, total }
211
+ }
212
+
213
+ async getEntityDetails(projectId: string, entityId: string): Promise<EntityDetailsOutput | null> {
214
+ const db = await this.database.forProject(projectId)
215
+
216
+ const entity = await db.entity.findUnique({
217
+ where: { id: entityId },
218
+ select: {
219
+ id: true,
220
+ type: true,
221
+ identity: true,
222
+ snapshots: {
223
+ orderBy: { createdAt: "desc" },
224
+ take: 1,
225
+ select: {
226
+ id: true,
227
+ content: {
228
+ select: {
229
+ meta: true,
230
+ content: true,
231
+ },
232
+ },
233
+ operationId: true,
234
+ stateId: true,
235
+ referencedInOutputs: true,
236
+ exportedInOutputs: true,
237
+ createdAt: true,
238
+ },
239
+ },
240
+ },
241
+ })
242
+
243
+ if (!entity) return null
244
+
245
+ const lastSnapshot = entity.snapshots[0] ?? null
246
+ const meta = toCommonEntityMeta(lastSnapshot?.content.meta)
247
+
248
+ return entityDetailsOutputSchema.parse({
249
+ id: entity.id,
250
+ type: entity.type,
251
+ identity: entity.identity,
252
+ meta: meta.title ? meta : { ...meta, title: entity.identity },
253
+ createdAt: lastSnapshot?.createdAt,
254
+ lastSnapshot: lastSnapshot
255
+ ? {
256
+ id: lastSnapshot.id,
257
+ meta: toCommonEntityMeta(lastSnapshot.content.meta),
258
+ content: lastSnapshot.content.content,
259
+ operationId: lastSnapshot.operationId,
260
+ stateId: lastSnapshot.stateId,
261
+ referencedInOutputs: z.string().array().parse(lastSnapshot.referencedInOutputs),
262
+ exportedInOutputs: z.string().array().parse(lastSnapshot.exportedInOutputs),
263
+ createdAt: lastSnapshot.createdAt,
264
+ }
265
+ : null,
266
+ })
267
+ }
268
+
269
+ async getEntitySnapshotDetails(
270
+ projectId: string,
271
+ snapshotId: string,
272
+ ): Promise<EntitySnapshotDetailsOutput | null> {
273
+ const db = await this.database.forProject(projectId)
274
+
275
+ const snapshot = await db.entitySnapshot.findUnique({
276
+ where: { id: snapshotId },
277
+ select: {
278
+ id: true,
279
+ content: {
280
+ select: {
281
+ meta: true,
282
+ content: true,
283
+ },
284
+ },
285
+ operationId: true,
286
+ stateId: true,
287
+ referencedInOutputs: true,
288
+ exportedInOutputs: true,
289
+ createdAt: true,
290
+ entity: {
291
+ select: {
292
+ id: true,
293
+ type: true,
294
+ identity: true,
295
+ },
296
+ },
297
+ },
298
+ })
299
+
300
+ if (!snapshot) return null
301
+
302
+ const meta = toCommonEntityMeta(snapshot.content.meta)
303
+
304
+ return entitySnapshotDetailsOutputSchema.parse({
305
+ entity: {
306
+ id: snapshot.entity.id,
307
+ type: snapshot.entity.type,
308
+ identity: snapshot.entity.identity,
309
+ },
310
+ snapshot: {
311
+ id: snapshot.id,
312
+ meta: meta.title ? meta : { ...meta, title: snapshot.entity.identity },
313
+ content: snapshot.content.content,
314
+ operationId: snapshot.operationId,
315
+ stateId: snapshot.stateId,
316
+ referencedInOutputs: z.string().array().parse(snapshot.referencedInOutputs),
317
+ exportedInOutputs: z.string().array().parse(snapshot.exportedInOutputs),
318
+ createdAt: snapshot.createdAt,
319
+ },
320
+ })
321
+ }
322
+
323
+ async queryEntitySnapshotsForEntity(
324
+ projectId: string,
325
+ entityId: string,
326
+ query: CollectionQuery,
327
+ excludeSnapshotId?: string,
328
+ ): Promise<CollectionQueryResult<EntitySnapshotListItemOutput>> {
329
+ const parsedQuery = collectionQuerySchema.parse(query)
330
+ const db = await this.database.forProject(projectId)
331
+
332
+ const whereClause = {
333
+ entityId,
334
+ ...(excludeSnapshotId ? { NOT: { id: excludeSnapshotId } } : {}),
335
+ ...(parsedQuery.search
336
+ ? {
337
+ OR: [
338
+ { id: { contains: parsedQuery.search } },
339
+ { operationId: { contains: parsedQuery.search } },
340
+ { stateId: { contains: parsedQuery.search } },
341
+ ],
342
+ }
343
+ : {}),
344
+ }
345
+
346
+ const [total, items] = await Promise.all([
347
+ db.entitySnapshot.count({ where: whereClause }),
348
+ db.entitySnapshot.findMany({
349
+ where: whereClause,
350
+ orderBy: { createdAt: "desc" },
351
+ skip: parsedQuery.skip,
352
+ take: parsedQuery.count,
353
+ select: {
354
+ id: true,
355
+ content: {
356
+ select: {
357
+ meta: true,
358
+ },
359
+ },
360
+ operationId: true,
361
+ stateId: true,
362
+ createdAt: true,
363
+ entity: {
364
+ select: {
365
+ identity: true,
366
+ },
367
+ },
368
+ },
369
+ }),
370
+ ])
371
+
372
+ const outputItems = items.map(item => {
373
+ const meta = toCommonEntityMeta(item.content.meta)
374
+
375
+ return entitySnapshotListItemOutputSchema.parse({
376
+ id: item.id,
377
+ meta: meta.title ? meta : { ...meta, title: item.entity.identity },
378
+ operationId: item.operationId,
379
+ stateId: item.stateId,
380
+ createdAt: item.createdAt,
381
+ })
382
+ })
383
+
384
+ return { items: outputItems, total }
385
+ }
386
+
387
+ async queryEntitySnapshotsForInstanceOperation(
388
+ projectId: string,
389
+ stateId: string,
390
+ operationId: string,
391
+ query: CollectionQuery,
392
+ ): Promise<CollectionQueryResult<EntitySnapshotListItemOutput>> {
393
+ const parsedQuery = collectionQuerySchema.parse(query)
394
+ const db = await this.database.forProject(projectId)
395
+
396
+ const whereClause = {
397
+ stateId,
398
+ operationId,
399
+ ...(parsedQuery.search
400
+ ? {
401
+ OR: [
402
+ { id: { contains: parsedQuery.search } },
403
+ { entityId: { contains: parsedQuery.search } },
404
+ { entity: { identity: { contains: parsedQuery.search } } },
405
+ { entity: { type: { contains: parsedQuery.search } } },
406
+ ],
407
+ }
408
+ : {}),
409
+ }
410
+
411
+ const [total, items] = await Promise.all([
412
+ db.entitySnapshot.count({ where: whereClause }),
413
+ db.entitySnapshot.findMany({
414
+ where: whereClause,
415
+ orderBy: { createdAt: "desc" },
416
+ skip: parsedQuery.skip,
417
+ take: parsedQuery.count,
418
+ select: {
419
+ id: true,
420
+ content: {
421
+ select: {
422
+ meta: true,
423
+ },
424
+ },
425
+ operationId: true,
426
+ stateId: true,
427
+ createdAt: true,
428
+ entity: {
429
+ select: {
430
+ identity: true,
431
+ },
432
+ },
433
+ },
434
+ }),
435
+ ])
436
+
437
+ const outputItems = items.map(item => {
438
+ const meta = toCommonEntityMeta(item.content.meta)
439
+
440
+ return entitySnapshotListItemOutputSchema.parse({
441
+ id: item.id,
442
+ meta: meta.title ? meta : { ...meta, title: item.entity.identity },
443
+ operationId: item.operationId,
444
+ stateId: item.stateId,
445
+ createdAt: item.createdAt,
446
+ })
447
+ })
448
+
449
+ return { items: outputItems, total }
450
+ }
451
+
452
+ async queryEntitySnapshotOutgoingReferences(
453
+ projectId: string,
454
+ snapshotId: string,
455
+ query: CollectionQuery,
456
+ ): Promise<CollectionQueryResult<EntityReferenceOutput>> {
457
+ const db = await this.database.forProject(projectId)
458
+
459
+ const snapshot = await db.entitySnapshot.findUnique({
460
+ where: { id: snapshotId },
461
+ select: {
462
+ id: true,
463
+ content: {
464
+ select: {
465
+ meta: true,
466
+ },
467
+ },
468
+ entity: {
469
+ select: {
470
+ id: true,
471
+ type: true,
472
+ identity: true,
473
+ },
474
+ },
475
+ },
476
+ })
477
+
478
+ if (!snapshot) return { items: [], total: 0 }
479
+
480
+ const whereClause = this.buildEntitySnapshotWhere(query, "fromId", snapshot.id)
481
+
482
+ const [total, items] = await Promise.all([
483
+ db.entitySnapshotReference.count({ where: whereClause }),
484
+ db.entitySnapshotReference.findMany({
485
+ where: whereClause,
486
+ orderBy: this.buildEntitySnapshotOrderBy(query),
487
+ skip: query.skip,
488
+ take: query.count,
489
+ select: {
490
+ kind: true,
491
+ group: true,
492
+ fromId: true,
493
+ toId: true,
494
+ to: {
495
+ select: {
496
+ id: true,
497
+ content: {
498
+ select: {
499
+ meta: true,
500
+ },
501
+ },
502
+ entity: {
503
+ select: {
504
+ id: true,
505
+ type: true,
506
+ identity: true,
507
+ },
508
+ },
509
+ },
510
+ },
511
+ },
512
+ }),
513
+ ])
514
+
515
+ const fromMeta = toCommonEntityMeta(snapshot.content.meta)
516
+
517
+ const outputItems = items.map(item => {
518
+ const toEntity = item.to.entity
519
+ const toMeta = toCommonEntityMeta(item.to.content.meta)
520
+
521
+ return entityReferenceOutputSchema.parse({
522
+ id: `${item.fromId}:${item.toId}:${item.kind}:${item.group}`,
523
+ meta: toMeta.title ? toMeta : { ...toMeta, title: toEntity.identity },
524
+ group: item.group,
525
+
526
+ fromSnapshotId: item.fromId,
527
+ fromEntityId: snapshot.entity.id,
528
+ fromEntityType: snapshot.entity.type,
529
+ fromEntityIdentity: snapshot.entity.identity,
530
+ fromEntityMeta: fromMeta.title
531
+ ? fromMeta
532
+ : { ...fromMeta, title: snapshot.entity.identity },
533
+
534
+ toSnapshotId: item.toId,
535
+ toEntityId: toEntity.id,
536
+ toEntityType: toEntity.type,
537
+ toEntityIdentity: toEntity.identity,
538
+ toEntityMeta: toMeta.title ? toMeta : { ...toMeta, title: toEntity.identity },
539
+ })
540
+ })
541
+
542
+ return { items: outputItems, total }
543
+ }
544
+
545
+ async queryEntitySnapshotIncomingReferences(
546
+ projectId: string,
547
+ snapshotId: string,
548
+ query: CollectionQuery,
549
+ ): Promise<CollectionQueryResult<EntityReferenceOutput>> {
550
+ const db = await this.database.forProject(projectId)
551
+
552
+ const snapshot = await db.entitySnapshot.findUnique({
553
+ where: { id: snapshotId },
554
+ select: {
555
+ id: true,
556
+ content: {
557
+ select: {
558
+ meta: true,
559
+ },
560
+ },
561
+ entity: {
562
+ select: {
563
+ id: true,
564
+ type: true,
565
+ identity: true,
566
+ },
567
+ },
568
+ },
569
+ })
570
+
571
+ if (!snapshot) return { items: [], total: 0 }
572
+
573
+ const whereClause = this.buildEntitySnapshotWhere(query, "toId", snapshot.id)
574
+
575
+ const [total, items] = await Promise.all([
576
+ db.entitySnapshotReference.count({ where: whereClause }),
577
+ db.entitySnapshotReference.findMany({
578
+ where: whereClause,
579
+ orderBy: this.buildEntitySnapshotOrderBy(query),
580
+ skip: query.skip,
581
+ take: query.count,
582
+ select: {
583
+ kind: true,
584
+ group: true,
585
+ fromId: true,
586
+ toId: true,
587
+ from: {
588
+ select: {
589
+ id: true,
590
+ content: {
591
+ select: {
592
+ meta: true,
593
+ },
594
+ },
595
+ entity: {
596
+ select: {
597
+ id: true,
598
+ type: true,
599
+ identity: true,
600
+ },
601
+ },
602
+ },
603
+ },
604
+ },
605
+ }),
606
+ ])
607
+
608
+ const toMeta = toCommonEntityMeta(snapshot.content.meta)
609
+
610
+ const outputItems = items.map(item => {
611
+ const fromEntity = item.from.entity
612
+ const fromMeta = toCommonEntityMeta(item.from.content.meta)
613
+
614
+ return entityReferenceOutputSchema.parse({
615
+ id: `${item.fromId}:${item.toId}:${item.kind}:${item.group}`,
616
+ meta: fromMeta.title ? fromMeta : { ...fromMeta, title: fromEntity.identity },
617
+ group: item.group,
618
+
619
+ fromSnapshotId: item.fromId,
620
+ fromEntityId: fromEntity.id,
621
+ fromEntityType: fromEntity.type,
622
+ fromEntityIdentity: fromEntity.identity,
623
+ fromEntityMeta: fromMeta.title ? fromMeta : { ...fromMeta, title: fromEntity.identity },
624
+
625
+ toSnapshotId: item.toId,
626
+ toEntityId: snapshot.entity.id,
627
+ toEntityType: snapshot.entity.type,
628
+ toEntityIdentity: snapshot.entity.identity,
629
+ toEntityMeta: toMeta.title ? toMeta : { ...toMeta, title: snapshot.entity.identity },
630
+ })
631
+ })
632
+
633
+ return { items: outputItems, total }
634
+ }
635
+
636
+ async queryEntityOutgoingReferences(
637
+ projectId: string,
638
+ entityId: string,
639
+ query: CollectionQuery,
640
+ ): Promise<CollectionQueryResult<EntityReferenceOutput>> {
641
+ const db = await this.database.forProject(projectId)
642
+
643
+ const entity = await db.entity.findUnique({
644
+ where: { id: entityId },
645
+ select: {
646
+ id: true,
647
+ type: true,
648
+ identity: true,
649
+ snapshots: {
650
+ orderBy: { createdAt: "desc" },
651
+ take: 1,
652
+ select: {
653
+ id: true,
654
+ content: {
655
+ select: {
656
+ meta: true,
657
+ },
658
+ },
659
+ },
660
+ },
661
+ },
662
+ })
663
+
664
+ if (!entity) return { items: [], total: 0 }
665
+
666
+ const lastSnapshot = entity.snapshots[0] ?? null
667
+ if (!lastSnapshot) return { items: [], total: 0 }
668
+
669
+ const whereClause = this.buildEntitySnapshotWhere(query, "fromId", lastSnapshot.id)
670
+
671
+ const [total, items] = await Promise.all([
672
+ db.entitySnapshotReference.count({ where: whereClause }),
673
+ db.entitySnapshotReference.findMany({
674
+ where: whereClause,
675
+ orderBy: this.buildEntitySnapshotOrderBy(query),
676
+ skip: query.skip,
677
+ take: query.count,
678
+ select: {
679
+ kind: true,
680
+ group: true,
681
+ fromId: true,
682
+ toId: true,
683
+ to: {
684
+ select: {
685
+ id: true,
686
+ content: {
687
+ select: {
688
+ meta: true,
689
+ },
690
+ },
691
+ entity: {
692
+ select: {
693
+ id: true,
694
+ type: true,
695
+ identity: true,
696
+ },
697
+ },
698
+ },
699
+ },
700
+ },
701
+ }),
702
+ ])
703
+
704
+ const fromMeta = toCommonEntityMeta(lastSnapshot.content.meta)
705
+
706
+ const outputItems = items.map(item => {
707
+ const toEntity = item.to.entity
708
+ const toMeta = toCommonEntityMeta(item.to.content.meta)
709
+
710
+ return entityReferenceOutputSchema.parse({
711
+ id: `${item.fromId}:${item.toId}:${item.kind}:${item.group}`,
712
+ meta: toMeta.title ? toMeta : { ...toMeta, title: toEntity.identity },
713
+ group: item.group,
714
+
715
+ fromSnapshotId: item.fromId,
716
+ fromEntityId: entity.id,
717
+ fromEntityType: entity.type,
718
+ fromEntityIdentity: entity.identity,
719
+ fromEntityMeta: fromMeta.title ? fromMeta : { ...fromMeta, title: entity.identity },
720
+
721
+ toSnapshotId: item.toId,
722
+ toEntityId: toEntity.id,
723
+ toEntityType: toEntity.type,
724
+ toEntityIdentity: toEntity.identity,
725
+ toEntityMeta: toMeta.title ? toMeta : { ...toMeta, title: toEntity.identity },
726
+ })
727
+ })
728
+
729
+ return { items: outputItems, total }
730
+ }
731
+
732
+ async queryEntityIncomingReferences(
733
+ projectId: string,
734
+ entityId: string,
735
+ query: CollectionQuery,
736
+ ): Promise<CollectionQueryResult<EntityReferenceOutput>> {
737
+ const db = await this.database.forProject(projectId)
738
+
739
+ const entity = await db.entity.findUnique({
740
+ where: { id: entityId },
741
+ select: {
742
+ id: true,
743
+ type: true,
744
+ identity: true,
745
+ snapshots: {
746
+ orderBy: { createdAt: "desc" },
747
+ take: 1,
748
+ select: {
749
+ id: true,
750
+ content: {
751
+ select: {
752
+ meta: true,
753
+ },
754
+ },
755
+ },
756
+ },
757
+ },
758
+ })
759
+
760
+ if (!entity) return { items: [], total: 0 }
761
+
762
+ const lastSnapshot = entity.snapshots[0] ?? null
763
+ if (!lastSnapshot) return { items: [], total: 0 }
764
+
765
+ const whereClause = this.buildEntitySnapshotWhere(query, "toId", lastSnapshot.id)
766
+
767
+ const [total, items] = await Promise.all([
768
+ db.entitySnapshotReference.count({ where: whereClause }),
769
+ db.entitySnapshotReference.findMany({
770
+ where: whereClause,
771
+ orderBy: this.buildEntitySnapshotOrderBy(query),
772
+ skip: query.skip,
773
+ take: query.count,
774
+ select: {
775
+ kind: true,
776
+ group: true,
777
+ fromId: true,
778
+ toId: true,
779
+ from: {
780
+ select: {
781
+ id: true,
782
+ content: {
783
+ select: {
784
+ meta: true,
785
+ },
786
+ },
787
+ entity: {
788
+ select: {
789
+ id: true,
790
+ type: true,
791
+ identity: true,
792
+ },
793
+ },
794
+ },
795
+ },
796
+ },
797
+ }),
798
+ ])
799
+
800
+ const toMeta = toCommonEntityMeta(lastSnapshot.content.meta)
801
+
802
+ const outputItems = items.map(item => {
803
+ const fromEntity = item.from.entity
804
+ const fromMeta = toCommonEntityMeta(item.from.content.meta)
805
+
806
+ return entityReferenceOutputSchema.parse({
807
+ id: `${item.fromId}:${item.toId}:${item.kind}:${item.group}`,
808
+ meta: fromMeta.title ? fromMeta : { ...fromMeta, title: fromEntity.identity },
809
+ group: item.group,
810
+
811
+ fromSnapshotId: item.fromId,
812
+ fromEntityId: fromEntity.id,
813
+ fromEntityType: fromEntity.type,
814
+ fromEntityIdentity: fromEntity.identity,
815
+ fromEntityMeta: fromMeta.title ? fromMeta : { ...fromMeta, title: fromEntity.identity },
816
+
817
+ toSnapshotId: item.toId,
818
+ toEntityId: entity.id,
819
+ toEntityType: entity.type,
820
+ toEntityIdentity: entity.identity,
821
+ toEntityMeta: toMeta.title ? toMeta : { ...toMeta, title: entity.identity },
822
+ })
823
+ })
824
+
825
+ return { items: outputItems, total }
826
+ }
827
+
70
828
  private buildOperationWhere(query: CollectionQuery): OperationWhereInput {
71
829
  if (!query.search) return {}
72
830
 
@@ -330,6 +1088,9 @@ export class SettingsService {
330
1088
 
331
1089
  if (query.sortBy.length === 1) {
332
1090
  const sort = query.sortBy[0]
1091
+ if (!sort) {
1092
+ return { [defaultField]: "desc" }
1093
+ }
333
1094
  return { [sort.key]: sort.order }
334
1095
  }
335
1096