@highstate/backend 0.19.1 → 0.21.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.
Files changed (134) hide show
  1. package/dist/chunk-b05q6fm2.js +37 -0
  2. package/dist/{chunk-V2NILDHS.js → chunk-gxjwa93h.js} +704 -604
  3. package/dist/{chunk-X2WG3WGL.js → chunk-vzdz6chj.js} +18 -15
  4. package/dist/highstate.manifest.json +4 -4
  5. package/dist/index.js +7350 -3514
  6. package/dist/library/package-resolution-worker.js +121 -10
  7. package/dist/library/worker/main.js +31 -17
  8. package/dist/shared/index.js +254 -4
  9. package/package.json +19 -20
  10. package/prisma/backend/_schema/object.prisma +12 -0
  11. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  12. package/prisma/project/artifact.prisma +3 -0
  13. package/prisma/project/entity.prisma +125 -0
  14. package/prisma/project/instance.prisma +6 -0
  15. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  16. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  17. package/prisma/project/operation.prisma +3 -0
  18. package/src/artifact/factory.ts +3 -2
  19. package/src/business/artifact.test.ts +22 -2
  20. package/src/business/artifact.ts +7 -1
  21. package/src/business/entity-snapshot.test.ts +684 -0
  22. package/src/business/entity-snapshot.ts +904 -0
  23. package/src/business/evaluation.test.ts +56 -0
  24. package/src/business/evaluation.ts +102 -22
  25. package/src/business/global-search.test.ts +344 -0
  26. package/src/business/global-search.ts +902 -0
  27. package/src/business/index.ts +4 -0
  28. package/src/business/instance-lock.ts +58 -74
  29. package/src/business/instance-state.test.ts +15 -1
  30. package/src/business/instance-state.ts +37 -14
  31. package/src/business/object-ref-index.test.ts +140 -0
  32. package/src/business/object-ref-index.ts +193 -0
  33. package/src/business/operation.test.ts +15 -1
  34. package/src/business/operation.ts +4 -0
  35. package/src/business/project-model.ts +154 -13
  36. package/src/business/project-unlock.ts +25 -2
  37. package/src/business/project.ts +9 -0
  38. package/src/business/secret.test.ts +35 -2
  39. package/src/business/secret.ts +32 -9
  40. package/src/business/settings.ts +761 -0
  41. package/src/business/unit-output.test.ts +477 -0
  42. package/src/business/unit-output.ts +461 -0
  43. package/src/business/worker.ts +55 -4
  44. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  45. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  47. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  49. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  50. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  51. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  52. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  54. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  56. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  57. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  58. package/src/database/_generated/project/browser.ts +23 -0
  59. package/src/database/_generated/project/client.ts +23 -0
  60. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  61. package/src/database/_generated/project/enums.ts +8 -0
  62. package/src/database/_generated/project/internal/class.ts +53 -5
  63. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  64. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  65. package/src/database/_generated/project/models/Artifact.ts +199 -11
  66. package/src/database/_generated/project/models/Entity.ts +1274 -0
  67. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  68. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  69. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  70. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  71. package/src/database/_generated/project/models/Operation.ts +148 -3
  72. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  73. package/src/database/_generated/project/models.ts +4 -0
  74. package/src/database/migration.ts +3 -0
  75. package/src/library/find-package-json.test.ts +77 -0
  76. package/src/library/find-package-json.ts +149 -0
  77. package/src/library/package-resolution-worker.ts +7 -3
  78. package/src/library/worker/evaluator.ts +7 -1
  79. package/src/orchestrator/manager.ts +7 -0
  80. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  81. package/src/orchestrator/operation-context.ts +154 -16
  82. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  83. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  84. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  85. package/src/orchestrator/operation-plan.md +4 -1
  86. package/src/orchestrator/operation-plan.ts +286 -92
  87. package/src/orchestrator/operation-plan.update.test.md +286 -11
  88. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  89. package/src/orchestrator/operation-workset.ts +72 -22
  90. package/src/orchestrator/operation.cancel.test.ts +4 -0
  91. package/src/orchestrator/operation.composite.test.ts +341 -0
  92. package/src/orchestrator/operation.destroy.test.ts +4 -0
  93. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  94. package/src/orchestrator/operation.preview.test.ts +4 -0
  95. package/src/orchestrator/operation.refresh.test.ts +4 -0
  96. package/src/orchestrator/operation.test-utils.ts +52 -13
  97. package/src/orchestrator/operation.ts +230 -68
  98. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  99. package/src/orchestrator/operation.update.skip.test.ts +196 -0
  100. package/src/orchestrator/operation.update.test.ts +4 -0
  101. package/src/orchestrator/plan-test-builder.ts +1 -0
  102. package/src/orchestrator/unit-input-values.test.ts +450 -0
  103. package/src/orchestrator/unit-input-values.ts +281 -0
  104. package/src/pubsub/manager.ts +3 -0
  105. package/src/runner/abstractions.ts +23 -54
  106. package/src/runner/factory.ts +3 -3
  107. package/src/runner/force-abort.ts +7 -2
  108. package/src/runner/local.ts +116 -87
  109. package/src/runner/pulumi.ts +3 -5
  110. package/src/services.ts +53 -2
  111. package/src/shared/models/prisma.ts +1 -0
  112. package/src/shared/models/project/entity.ts +121 -0
  113. package/src/shared/models/project/index.ts +1 -0
  114. package/src/shared/models/project/operation.ts +61 -3
  115. package/src/shared/models/project/state.ts +10 -0
  116. package/src/shared/models/project/worker.ts +7 -0
  117. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  118. package/src/shared/resolvers/effective-output-type.ts +162 -0
  119. package/src/shared/resolvers/index.ts +1 -0
  120. package/src/shared/resolvers/input.ts +59 -9
  121. package/src/shared/utils/index.ts +1 -0
  122. package/src/shared/utils/stable-json.ts +41 -0
  123. package/src/terminal/manager.ts +6 -0
  124. package/src/terminal/run.sh.ts +9 -4
  125. package/src/worker/manager.ts +97 -1
  126. package/LICENSE +0 -21
  127. package/dist/chunk-I7BWSAN6.js +0 -49
  128. package/dist/chunk-I7BWSAN6.js.map +0 -1
  129. package/dist/chunk-V2NILDHS.js.map +0 -1
  130. package/dist/chunk-X2WG3WGL.js.map +0 -1
  131. package/dist/index.js.map +0 -1
  132. package/dist/library/package-resolution-worker.js.map +0 -1
  133. package/dist/library/worker/main.js.map +0 -1
  134. package/dist/shared/index.js.map +0 -1
@@ -1,8 +1,11 @@
1
1
  export * from "./api-key"
2
2
  export * from "./artifact"
3
3
  export * from "./backend-unlock"
4
+ export * from "./entity-snapshot"
5
+ export * from "./global-search"
4
6
  export * from "./instance-lock"
5
7
  export * from "./instance-state"
8
+ export * from "./object-ref-index"
6
9
  export * from "./operation"
7
10
  export * from "./project"
8
11
  export * from "./project-model"
@@ -11,4 +14,5 @@ export * from "./secret"
11
14
  export * from "./settings"
12
15
  export * from "./terminal-session"
13
16
  export * from "./unit-extra"
17
+ export * from "./unit-output"
14
18
  export * from "./worker"
@@ -260,79 +260,59 @@ export class InstanceLockService {
260
260
  let remainingStateIds = [...stateIds]
261
261
  const lockedStateIds: string[] = []
262
262
 
263
- // create abort controller for managing event subscription
264
- const subscriptionController = new AbortController()
263
+ while (remainingStateIds.length > 0) {
264
+ if (abortSignal?.aborted) {
265
+ throw new Error("Lock operation was aborted")
266
+ }
265
267
 
266
- // set up event subscription first before attempting any locks to reduce probability of missing events
267
- const eventIterable = await this.pubsubManager.subscribe(
268
- ["instance-lock", projectId],
269
- subscriptionController.signal,
270
- )
268
+ this.logger.debug(
269
+ {
270
+ projectId,
271
+ remainingCount: remainingStateIds.length,
272
+ lockedCount: lockedStateIds.length,
273
+ },
274
+ "attempting to lock %s remaining instances",
275
+ remainingStateIds.length,
276
+ )
271
277
 
272
- try {
273
- while (remainingStateIds.length > 0) {
274
- if (abortSignal?.aborted) {
275
- throw new Error("Lock operation was aborted")
276
- }
278
+ // try to acquire locks on remaining instances using the same token
279
+ const [_, newlyLockedStateIds] = await this.tryLockInstances(
280
+ projectId,
281
+ remainingStateIds,
282
+ lockMeta,
283
+ action,
284
+ allowPartialLock,
285
+ token,
286
+ )
277
287
 
288
+ if (newlyLockedStateIds.length === 0) {
289
+ // no instances were locked, wait for unlock events
278
290
  this.logger.debug(
279
- {
280
- projectId,
281
- remainingCount: remainingStateIds.length,
282
- lockedCount: lockedStateIds.length,
283
- },
284
- "attempting to lock %s remaining instances",
291
+ { projectId, remainingCount: remainingStateIds.length },
292
+ "waiting for unlock events for %s remaining instances",
285
293
  remainingStateIds.length,
286
294
  )
287
295
 
288
- // try to acquire locks on remaining instances using the same token
289
- const [_, newlyLockedStateIds] = await this.tryLockInstances(
290
- projectId,
291
- remainingStateIds,
292
- lockMeta,
293
- action,
294
- allowPartialLock,
295
- token,
296
- )
296
+ await this.waitForUnlockEvent(projectId, remainingStateIds, abortSignal, eventWaitTime)
297
+ continue
298
+ }
297
299
 
298
- if (newlyLockedStateIds.length === 0) {
299
- // no instances were locked, wait for unlock events
300
- this.logger.debug(
301
- { projectId, remainingCount: remainingStateIds.length },
302
- "waiting for unlock events for %s remaining instances",
303
- remainingStateIds.length,
304
- )
300
+ // remove newly locked instances from remaining list
301
+ remainingStateIds = remainingStateIds.filter(id => !newlyLockedStateIds.includes(id))
302
+ lockedStateIds.push(...newlyLockedStateIds)
305
303
 
306
- await this.waitForUnlockEvent(
307
- projectId,
308
- remainingStateIds,
309
- eventIterable,
310
- abortSignal,
311
- eventWaitTime,
312
- )
313
- continue
314
- }
315
-
316
- // remove newly locked instances from remaining list
317
- remainingStateIds = remainingStateIds.filter(id => !newlyLockedStateIds.includes(id))
318
- lockedStateIds.push(...newlyLockedStateIds)
319
-
320
- // if partial locking is not allowed, we should have all instances by now
321
- if (!allowPartialLock && remainingStateIds.length > 0) {
322
- this.logger.error(
323
- { projectId, remaining: remainingStateIds.length },
324
- "partial lock not allowed but %s instances remain unlocked",
325
- remainingStateIds.length,
326
- )
327
- throw new Error("Failed to acquire all required locks")
328
- }
304
+ // if partial locking is not allowed, we should have all instances by now
305
+ if (!allowPartialLock && remainingStateIds.length > 0) {
306
+ this.logger.error(
307
+ { projectId, remaining: remainingStateIds.length },
308
+ "partial lock not allowed but %s instances remain unlocked",
309
+ remainingStateIds.length,
310
+ )
311
+ throw new Error("Failed to acquire all required locks")
329
312
  }
330
-
331
- return [token, lockedStateIds]
332
- } finally {
333
- // clean up event subscription
334
- subscriptionController.abort()
335
313
  }
314
+
315
+ return [token, lockedStateIds]
336
316
  }
337
317
 
338
318
  /**
@@ -341,32 +321,36 @@ export class InstanceLockService {
341
321
  *
342
322
  * @param projectId The project ID to monitor for events.
343
323
  * @param stateIds The state IDs we're waiting to become available.
344
- * @param eventIterable The async iterable for event subscription.
345
324
  * @param abortSignal Optional abort signal to interrupt waiting.
346
325
  * @param eventWaitTime Time in milliseconds to wait before timing out and retrying.
347
326
  */
348
327
  private async waitForUnlockEvent(
349
328
  projectId: string,
350
329
  stateIds: string[],
351
- eventIterable: AsyncIterable<InstanceLockEvent>,
352
330
  abortSignal?: AbortSignal,
353
331
  eventWaitTime = 60000,
354
332
  ): Promise<void> {
355
- const eventController = new AbortController()
333
+ const waitController = new AbortController()
334
+ const eventIterable = await this.pubsubManager.subscribe(
335
+ ["instance-lock", projectId],
336
+ waitController.signal,
337
+ )
356
338
 
357
339
  // combine abort signals
358
340
  if (abortSignal?.aborted) {
359
341
  throw new Error("Lock operation was aborted")
360
342
  }
361
343
 
362
- const abortHandler = () => eventController.abort()
344
+ const abortHandler = () => waitController.abort()
345
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
346
+
363
347
  abortSignal?.addEventListener("abort", abortHandler)
364
348
 
365
349
  try {
366
350
  await Promise.race([
367
351
  // timeout promise - triggers retry attempt, does not abort
368
352
  new Promise<void>(resolve => {
369
- setTimeout(() => {
353
+ timeoutId = setTimeout(() => {
370
354
  this.logger.debug(
371
355
  { projectId, eventWaitTime },
372
356
  "unlock wait timed out after %s ms, will retry",
@@ -377,7 +361,7 @@ export class InstanceLockService {
377
361
  }),
378
362
 
379
363
  // event listener promise
380
- this.listenForUnlockEvents(projectId, stateIds, eventIterable, eventController.signal),
364
+ this.listenForUnlockEvents(projectId, stateIds, eventIterable),
381
365
 
382
366
  // abort promise - only this can interrupt the operation
383
367
  new Promise<void>((_, reject) => {
@@ -389,7 +373,12 @@ export class InstanceLockService {
389
373
  }),
390
374
  ])
391
375
  } finally {
392
- eventController.abort()
376
+ waitController.abort()
377
+
378
+ if (timeoutId) {
379
+ clearTimeout(timeoutId)
380
+ }
381
+
393
382
  abortSignal?.removeEventListener("abort", abortHandler)
394
383
  }
395
384
  }
@@ -400,18 +389,13 @@ export class InstanceLockService {
400
389
  * @param projectId The project ID to monitor for events.
401
390
  * @param stateIds The state IDs we're waiting to become available.
402
391
  * @param eventIterable The async iterable for event subscription.
403
- * @param signal Abort signal to stop listening.
404
392
  */
405
393
  private async listenForUnlockEvents(
406
394
  projectId: string,
407
395
  stateIds: string[],
408
396
  eventIterable: AsyncIterable<InstanceLockEvent>,
409
- signal: AbortSignal,
410
397
  ): Promise<void> {
411
398
  for await (const event of eventIterable) {
412
- if (signal.aborted) {
413
- break
414
- }
415
399
  if (event.type !== "unlocked") {
416
400
  continue // only interested in unlock events
417
401
  }
@@ -2,6 +2,7 @@ import type { InstanceId, InstanceModel } from "@highstate/contract"
2
2
  import type { ArtifactService } from "../artifact"
3
3
  import type { PubSubManager } from "../pubsub"
4
4
  import type { RunnerBackend } from "../runner"
5
+ import type { ObjectRefIndexService } from "./object-ref-index"
5
6
  import type { ProjectService } from "./project"
6
7
  import type { SecretService } from "./secret"
7
8
  import type { UnitExtraService } from "./unit-extra"
@@ -21,6 +22,7 @@ const instanceStateTest = test.extend<{
21
22
  artifactService: MockedObject<ArtifactService>
22
23
  unitExtraService: MockedObject<UnitExtraService>
23
24
  secretService: MockedObject<SecretService>
25
+ objectRefIndexService: MockedObject<ObjectRefIndexService>
24
26
  instanceStateService: InstanceStateService
25
27
  }>({
26
28
  pubsubManager: async ({}, use) => {
@@ -52,6 +54,7 @@ const instanceStateTest = test.extend<{
52
54
  workerService: async ({}, use) => {
53
55
  const workerService = vi.mockObject({
54
56
  cleanupWorkerUsageAndSync: vi.fn().mockResolvedValue(undefined),
57
+ updateUnitRegistrations: vi.fn().mockResolvedValue([]),
55
58
  } as unknown as WorkerService)
56
59
 
57
60
  await use(workerService)
@@ -76,10 +79,19 @@ const instanceStateTest = test.extend<{
76
79
  },
77
80
 
78
81
  secretService: async ({}, use) => {
79
- const secretService = vi.mockObject({} as unknown as SecretService)
82
+ const secretService = vi.mockObject({
83
+ updateInstanceSecretsCore: vi.fn().mockResolvedValue({ secretNames: [], secretIds: [] }),
84
+ } as unknown as SecretService)
80
85
  await use(secretService)
81
86
  },
82
87
 
88
+ objectRefIndexService: async ({}, use) => {
89
+ const objectRefIndexService = vi.mockObject({
90
+ track: vi.fn().mockResolvedValue(undefined),
91
+ } as unknown as ObjectRefIndexService)
92
+ await use(objectRefIndexService)
93
+ },
94
+
83
95
  instanceStateService: async (
84
96
  {
85
97
  database,
@@ -89,6 +101,7 @@ const instanceStateTest = test.extend<{
89
101
  artifactService,
90
102
  unitExtraService,
91
103
  secretService,
104
+ objectRefIndexService,
92
105
  logger,
93
106
  },
94
107
  use,
@@ -101,6 +114,7 @@ const instanceStateTest = test.extend<{
101
114
  artifactService,
102
115
  unitExtraService,
103
116
  secretService,
117
+ objectRefIndexService,
104
118
  logger.child({ service: "InstanceStateService" }),
105
119
  )
106
120
 
@@ -10,6 +10,7 @@ import type { ArtifactService } from "../artifact"
10
10
  import type { SecretService, UnitExtraService, WorkerService } from "../business"
11
11
  import type { PubSubManager } from "../pubsub"
12
12
  import type { RunnerBackend } from "../runner"
13
+ import type { ObjectRefIndexService } from "./object-ref-index"
13
14
  import { type InstanceId, parseInstanceId } from "@highstate/contract"
14
15
  import { isNonNullish, omit } from "remeda"
15
16
  import {
@@ -103,6 +104,7 @@ export type InstanceStatePatch = Pick<
103
104
  | "resolvedInputs"
104
105
  | "currentResourceCount"
105
106
  | "exportedArtifactIds"
107
+ | "hasResourceHooks"
106
108
  >
107
109
 
108
110
  export type UpdateOperationStateOptions = {
@@ -220,6 +222,7 @@ export class InstanceStateService {
220
222
  private readonly artifactService: ArtifactService,
221
223
  private readonly unitExtraService: UnitExtraService,
222
224
  private readonly secretService: SecretService,
225
+ private readonly objectRefIndexService: ObjectRefIndexService,
223
226
  private readonly logger: Logger,
224
227
  ) {}
225
228
 
@@ -667,6 +670,7 @@ export class InstanceStateService {
667
670
 
668
671
  const result = await database.$transaction(async tx => {
669
672
  let unitExtraData = null
673
+ let unitExtraTrackingIds: string[] = []
670
674
 
671
675
  // update operation state
672
676
  const updatedOperationState = await tx.instanceOperationState.update({
@@ -690,20 +694,35 @@ export class InstanceStateService {
690
694
 
691
695
  // update unit-specific data if provided
692
696
  if (unitExtra) {
693
- const [pageIds, terminalIds, triggerIds, secretNames] = await Promise.all([
694
- this.unitExtraService.processUnitPages(tx, stateId, unitExtra.pages),
695
- this.unitExtraService.processUnitTerminals(tx, stateId, unitExtra.terminals),
696
- this.unitExtraService.processUnitTriggers(tx, stateId, unitExtra.triggers),
697
- this.secretService.updateInstanceSecretsCore(
698
- tx,
699
- project.libraryId,
700
- stateId,
701
- unitExtra.secrets,
702
- ),
703
- this.workerService.updateUnitRegistrations(tx, projectId, stateId, unitExtra.workers),
704
- ])
697
+ const [pageIds, terminalIds, triggerIds, secretUpdate, workerObjectIds] = await Promise.all(
698
+ [
699
+ this.unitExtraService.processUnitPages(tx, stateId, unitExtra.pages),
700
+ this.unitExtraService.processUnitTerminals(tx, stateId, unitExtra.terminals),
701
+ this.unitExtraService.processUnitTriggers(tx, stateId, unitExtra.triggers),
702
+ this.secretService.updateInstanceSecretsCore(
703
+ tx,
704
+ project.libraryId,
705
+ stateId,
706
+ unitExtra.secrets,
707
+ ),
708
+ this.workerService.updateUnitRegistrations(tx, projectId, stateId, unitExtra.workers),
709
+ ],
710
+ )
711
+
712
+ unitExtraData = {
713
+ pageIds,
714
+ terminalIds,
715
+ triggerIds,
716
+ secretNames: secretUpdate.secretNames,
717
+ }
705
718
 
706
- unitExtraData = { pageIds, terminalIds, triggerIds, secretNames }
719
+ unitExtraTrackingIds = [
720
+ ...pageIds,
721
+ ...terminalIds,
722
+ ...triggerIds,
723
+ ...secretUpdate.secretIds,
724
+ ...workerObjectIds,
725
+ ]
707
726
 
708
727
  if (unitExtra.artifactIds !== undefined) {
709
728
  await this.unitExtraService.pruneInstanceArtifacts(tx, stateId, unitExtra.artifactIds)
@@ -718,9 +737,13 @@ export class InstanceStateService {
718
737
  })
719
738
  }
720
739
 
721
- return { updatedOperationState, unitExtraData }
740
+ return { updatedOperationState, unitExtraData, unitExtraTrackingIds }
722
741
  })
723
742
 
743
+ if (result.unitExtraTrackingIds.length > 0) {
744
+ await this.objectRefIndexService.track(projectId, result.unitExtraTrackingIds)
745
+ }
746
+
724
747
  if (options.unitExtra?.artifactIds !== undefined) {
725
748
  await this.artifactService.collectGarbage(projectId)
726
749
  }
@@ -0,0 +1,140 @@
1
+ import { createId } from "@paralleldrive/cuid2"
2
+ import { describe, expect } from "vitest"
3
+ import { test } from "../test-utils/services"
4
+ import { ObjectRefIndexService } from "./object-ref-index"
5
+
6
+ describe(ObjectRefIndexService.name, () => {
7
+ test("track inserts unique ids and ignores blanks", async ({ database, project, logger }) => {
8
+ const service = new ObjectRefIndexService(database, logger)
9
+
10
+ const id1 = createId()
11
+ const id2 = createId()
12
+
13
+ await service.track(project.id, [id1, ` ${id1} `, "", " ", id2])
14
+ await service.track(project.id, [id1, id2])
15
+
16
+ const refs = await database.backend.object.findMany({
17
+ where: { projectId: project.id },
18
+ select: { id: true },
19
+ })
20
+
21
+ expect(refs.map(r => r.id).sort()).toEqual([id1, id2].sort())
22
+ })
23
+
24
+ test("syncProject indexes curated object ids", async ({
25
+ database,
26
+ projectDatabase,
27
+ project,
28
+ logger,
29
+ }) => {
30
+ const service = new ObjectRefIndexService(database, logger)
31
+
32
+ const operation = await projectDatabase.operation.create({
33
+ data: {
34
+ meta: { title: "op" },
35
+ type: "update",
36
+ options: {},
37
+ requestedInstanceIds: [],
38
+ },
39
+ select: { id: true },
40
+ })
41
+
42
+ const state = await projectDatabase.instanceState.create({
43
+ data: {
44
+ instanceId: `component.v1:${createId()}`,
45
+ kind: "unit",
46
+ status: "undeployed",
47
+ source: "resident",
48
+ },
49
+ select: { id: true },
50
+ })
51
+
52
+ const artifact = await projectDatabase.artifact.create({
53
+ data: {
54
+ meta: { title: "artifact" },
55
+ hash: `sha256:${createId()}`,
56
+ size: 1,
57
+ chunkSize: 1,
58
+ },
59
+ select: { id: true },
60
+ })
61
+
62
+ const page = await projectDatabase.page.create({
63
+ data: {
64
+ meta: { title: "page" },
65
+ content: [],
66
+ },
67
+ select: { id: true },
68
+ })
69
+
70
+ const secret = await projectDatabase.secret.create({
71
+ data: {
72
+ meta: { title: "secret" },
73
+ content: { value: "x" },
74
+ },
75
+ select: { id: true },
76
+ })
77
+
78
+ const serviceAccount = await projectDatabase.serviceAccount.create({
79
+ data: {
80
+ meta: { title: "sa" },
81
+ },
82
+ select: { id: true },
83
+ })
84
+
85
+ const apiKey = await projectDatabase.apiKey.create({
86
+ data: {
87
+ meta: { title: "key" },
88
+ serviceAccountId: serviceAccount.id,
89
+ token: createId(),
90
+ },
91
+ select: { id: true },
92
+ })
93
+
94
+ await service.syncProject(project.id)
95
+
96
+ const refs = await database.backend.object.findMany({
97
+ where: { projectId: project.id },
98
+ select: { id: true },
99
+ })
100
+
101
+ const refIds = new Set(refs.map(r => r.id))
102
+ expect(refIds).toContain(operation.id)
103
+ expect(refIds).toContain(state.id)
104
+ expect(refIds).toContain(artifact.id)
105
+ expect(refIds).toContain(page.id)
106
+ expect(refIds).toContain(secret.id)
107
+ expect(refIds).toContain(serviceAccount.id)
108
+ expect(refIds).toContain(apiKey.id)
109
+ })
110
+
111
+ test("syncProject prunes stale object ids", async ({
112
+ database,
113
+ projectDatabase,
114
+ project,
115
+ logger,
116
+ }) => {
117
+ const service = new ObjectRefIndexService(database, logger)
118
+
119
+ const operation = await projectDatabase.operation.create({
120
+ data: {
121
+ meta: { title: "op" },
122
+ type: "update",
123
+ options: {},
124
+ requestedInstanceIds: [],
125
+ },
126
+ select: { id: true },
127
+ })
128
+
129
+ await service.syncProject(project.id)
130
+ await projectDatabase.operation.delete({ where: { id: operation.id } })
131
+ await service.syncProject(project.id)
132
+
133
+ const refs = await database.backend.object.findMany({
134
+ where: { projectId: project.id, id: operation.id },
135
+ select: { id: true },
136
+ })
137
+
138
+ expect(refs).toHaveLength(0)
139
+ })
140
+ })