@highstate/backend 0.18.0 → 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.
- package/dist/{chunk-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
- package/dist/chunk-52MY2TCE.js.map +1 -0
- package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
- package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +4159 -785
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +5 -2
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +2 -2
- package/package.json +7 -7
- package/prisma/backend/_schema/object.prisma +12 -0
- package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
- package/prisma/project/artifact.prisma +3 -0
- package/prisma/project/entity.prisma +125 -0
- package/prisma/project/instance.prisma +6 -0
- package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
- package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
- package/prisma/project/operation.prisma +3 -0
- package/src/business/artifact.test.ts +22 -2
- package/src/business/artifact.ts +7 -1
- package/src/business/entity-snapshot.test.ts +684 -0
- package/src/business/entity-snapshot.ts +904 -0
- package/src/business/evaluation.test.ts +56 -0
- package/src/business/evaluation.ts +102 -22
- package/src/business/global-search.test.ts +344 -0
- package/src/business/global-search.ts +902 -0
- package/src/business/index.ts +4 -0
- package/src/business/instance-lock.ts +58 -74
- package/src/business/instance-state.test.ts +15 -1
- package/src/business/instance-state.ts +37 -14
- package/src/business/object-ref-index.test.ts +140 -0
- package/src/business/object-ref-index.ts +193 -0
- package/src/business/operation.test.ts +15 -1
- package/src/business/operation.ts +4 -0
- package/src/business/project-model.ts +154 -13
- package/src/business/project-unlock.ts +25 -2
- package/src/business/project.ts +9 -0
- package/src/business/secret.test.ts +35 -2
- package/src/business/secret.ts +32 -9
- package/src/business/settings.ts +761 -0
- package/src/business/unit-output.test.ts +477 -0
- package/src/business/unit-output.ts +461 -0
- package/src/business/worker.ts +55 -4
- package/src/database/_generated/backend/postgresql/browser.ts +6 -0
- package/src/database/_generated/backend/postgresql/client.ts +6 -0
- package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
- package/src/database/_generated/backend/postgresql/models.ts +1 -0
- package/src/database/_generated/backend/sqlite/browser.ts +6 -0
- package/src/database/_generated/backend/sqlite/client.ts +6 -0
- package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
- package/src/database/_generated/backend/sqlite/models.ts +1 -0
- package/src/database/_generated/project/browser.ts +23 -0
- package/src/database/_generated/project/client.ts +23 -0
- package/src/database/_generated/project/commonInputTypes.ts +87 -53
- package/src/database/_generated/project/enums.ts +8 -0
- package/src/database/_generated/project/internal/class.ts +53 -5
- package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
- package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
- package/src/database/_generated/project/models/Artifact.ts +199 -11
- package/src/database/_generated/project/models/Entity.ts +1274 -0
- package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
- package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
- package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
- package/src/database/_generated/project/models/InstanceState.ts +361 -1
- package/src/database/_generated/project/models/Operation.ts +148 -3
- package/src/database/_generated/project/models/OperationLog.ts +0 -4
- package/src/database/_generated/project/models.ts +4 -0
- package/src/database/migration.ts +3 -0
- package/src/library/worker/evaluator.ts +7 -1
- package/src/orchestrator/manager.ts +7 -0
- package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
- package/src/orchestrator/operation-context.ts +154 -16
- package/src/orchestrator/operation-plan.destroy.test.md +33 -12
- package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
- package/src/orchestrator/operation-plan.fixtures.ts +2 -0
- package/src/orchestrator/operation-plan.md +4 -1
- package/src/orchestrator/operation-plan.ts +286 -92
- package/src/orchestrator/operation-plan.update.test.md +286 -11
- package/src/orchestrator/operation-plan.update.test.ts +656 -5
- package/src/orchestrator/operation-workset.ts +72 -22
- package/src/orchestrator/operation.cancel.test.ts +4 -0
- package/src/orchestrator/operation.composite.test.ts +341 -0
- package/src/orchestrator/operation.destroy.test.ts +4 -0
- package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
- package/src/orchestrator/operation.preview.test.ts +4 -0
- package/src/orchestrator/operation.refresh.test.ts +4 -0
- package/src/orchestrator/operation.test-utils.ts +52 -13
- package/src/orchestrator/operation.ts +228 -68
- package/src/orchestrator/operation.update.failure.test.ts +4 -0
- package/src/orchestrator/operation.update.skip.test.ts +110 -0
- package/src/orchestrator/operation.update.test.ts +4 -0
- package/src/orchestrator/plan-test-builder.ts +1 -0
- package/src/orchestrator/unit-input-values.test.ts +450 -0
- package/src/orchestrator/unit-input-values.ts +281 -0
- package/src/pubsub/manager.ts +3 -0
- package/src/runner/abstractions.ts +23 -54
- package/src/runner/local.ts +109 -85
- package/src/services.ts +52 -1
- package/src/shared/models/prisma.ts +1 -0
- package/src/shared/models/project/entity.ts +121 -0
- package/src/shared/models/project/index.ts +1 -0
- package/src/shared/models/project/operation.ts +61 -3
- package/src/shared/models/project/state.ts +10 -0
- package/src/shared/models/project/worker.ts +7 -0
- package/src/shared/resolvers/effective-output-type.test.ts +494 -0
- package/src/shared/resolvers/effective-output-type.ts +162 -0
- package/src/shared/resolvers/index.ts +1 -0
- package/src/shared/resolvers/input.ts +61 -9
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/stable-json.ts +41 -0
- package/src/terminal/manager.ts +6 -0
- package/src/worker/manager.ts +97 -1
- package/dist/chunk-JT4KWE3B.js.map +0 -1
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import type { ProjectDatabase } from "../database"
|
|
2
|
+
import type { ObjectRefIndexService } from "./object-ref-index"
|
|
3
|
+
import { getEntityId } from "@highstate/contract"
|
|
4
|
+
import { createId } from "@paralleldrive/cuid2"
|
|
5
|
+
import { describe, vi } from "vitest"
|
|
6
|
+
import { test } from "../test-utils"
|
|
7
|
+
import { EntitySnapshotService } from "./entity-snapshot"
|
|
8
|
+
|
|
9
|
+
const entitySnapshotTest = test.extend<{
|
|
10
|
+
entitySnapshotService: EntitySnapshotService
|
|
11
|
+
}>({
|
|
12
|
+
entitySnapshotService: async ({ database, logger }, use) => {
|
|
13
|
+
const service = new EntitySnapshotService(
|
|
14
|
+
database,
|
|
15
|
+
vi.mockObject({
|
|
16
|
+
track: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
} as unknown as ObjectRefIndexService),
|
|
18
|
+
logger.child({ service: "EntitySnapshotService" }),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
await use(service)
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
async function createOperation(projectDatabase: ProjectDatabase): Promise<{
|
|
26
|
+
id: string
|
|
27
|
+
}> {
|
|
28
|
+
const operation = await projectDatabase.operation.create({
|
|
29
|
+
data: {
|
|
30
|
+
id: createId(),
|
|
31
|
+
meta: { title: "Test Operation" },
|
|
32
|
+
type: "update",
|
|
33
|
+
options: {},
|
|
34
|
+
requestedInstanceIds: ["server.v1:test"],
|
|
35
|
+
startedAt: new Date(),
|
|
36
|
+
},
|
|
37
|
+
select: { id: true },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return operation
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("persistUnitEntitySnapshots", () => {
|
|
44
|
+
entitySnapshotTest(
|
|
45
|
+
"persists deterministic entities and snapshots",
|
|
46
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
47
|
+
const operation = await createOperation(projectDatabase)
|
|
48
|
+
const state = await createInstanceState(project.id)
|
|
49
|
+
|
|
50
|
+
const deterministicEntityId = getEntityId({
|
|
51
|
+
$meta: { type: "test.entity.v1", identity: "id-1" },
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const deterministicEntityId2 = getEntityId({
|
|
55
|
+
$meta: { type: "test.entity.v1", identity: "id-2" },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
59
|
+
projectId: project.id,
|
|
60
|
+
operationId: operation.id,
|
|
61
|
+
stateId: state.id,
|
|
62
|
+
payload: {
|
|
63
|
+
nodes: [
|
|
64
|
+
{
|
|
65
|
+
entityId: deterministicEntityId,
|
|
66
|
+
entityType: "test.entity.v1",
|
|
67
|
+
identity: "id-1",
|
|
68
|
+
meta: { title: "Deterministic" },
|
|
69
|
+
content: { value: "hello" },
|
|
70
|
+
referencedOutputs: [],
|
|
71
|
+
exportedOutputs: ["value"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
entityId: deterministicEntityId2,
|
|
75
|
+
entityType: "test.entity.v1",
|
|
76
|
+
identity: "id-2",
|
|
77
|
+
meta: { title: "Second" },
|
|
78
|
+
content: { value: "world" },
|
|
79
|
+
referencedOutputs: [],
|
|
80
|
+
exportedOutputs: ["value"],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
implicitReferences: [],
|
|
84
|
+
explicitReferences: [],
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const deterministicEntity = await projectDatabase.entity.findUnique({
|
|
89
|
+
where: { id: deterministicEntityId },
|
|
90
|
+
})
|
|
91
|
+
expect(deterministicEntity?.id).toBe(deterministicEntityId)
|
|
92
|
+
expect(deterministicEntity?.identity).toBe("id-1")
|
|
93
|
+
|
|
94
|
+
const snapshots = await projectDatabase.entitySnapshot.findMany({
|
|
95
|
+
where: { operationId: operation.id, stateId: state.id },
|
|
96
|
+
orderBy: { createdAt: "asc" },
|
|
97
|
+
})
|
|
98
|
+
expect(snapshots).toHaveLength(2)
|
|
99
|
+
|
|
100
|
+
const deterministicSnapshot = snapshots.find(s => s.entityId === deterministicEntityId)
|
|
101
|
+
expect(deterministicSnapshot).toBeDefined()
|
|
102
|
+
expect(deterministicSnapshot?.id).not.toBe(deterministicEntityId)
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
entitySnapshotTest(
|
|
107
|
+
"creates implicit + explicit snapshot references",
|
|
108
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
109
|
+
const operation = await createOperation(projectDatabase)
|
|
110
|
+
const state = await createInstanceState(project.id)
|
|
111
|
+
|
|
112
|
+
const referencedEntityId = getEntityId({
|
|
113
|
+
$meta: { type: "ref.v1", identity: "ref-1" },
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const fromEntityId = getEntityId({
|
|
117
|
+
$meta: { type: "from.v1", identity: "from-1" },
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
121
|
+
projectId: project.id,
|
|
122
|
+
operationId: operation.id,
|
|
123
|
+
stateId: state.id,
|
|
124
|
+
payload: {
|
|
125
|
+
nodes: [
|
|
126
|
+
{
|
|
127
|
+
entityId: fromEntityId,
|
|
128
|
+
entityType: "from.v1",
|
|
129
|
+
identity: "from-1",
|
|
130
|
+
meta: { title: "From" },
|
|
131
|
+
content: { v: 1 },
|
|
132
|
+
referencedOutputs: [],
|
|
133
|
+
exportedOutputs: ["out"],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
entityId: referencedEntityId,
|
|
137
|
+
entityType: "ref.v1",
|
|
138
|
+
identity: "ref-1",
|
|
139
|
+
meta: { title: "To" },
|
|
140
|
+
content: { v: 2 },
|
|
141
|
+
referencedOutputs: [],
|
|
142
|
+
exportedOutputs: ["out"],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
implicitReferences: [
|
|
146
|
+
{ fromEntityId: fromEntityId, toEntityId: referencedEntityId, group: "child" },
|
|
147
|
+
{ fromEntityId: fromEntityId, toEntityId: referencedEntityId, group: "child" },
|
|
148
|
+
],
|
|
149
|
+
explicitReferences: [
|
|
150
|
+
{ fromEntityId: fromEntityId, toEntityId: referencedEntityId, group: "deps" },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const allSnapshots = await projectDatabase.entitySnapshot.findMany({
|
|
156
|
+
where: { operationId: operation.id, stateId: state.id },
|
|
157
|
+
select: { id: true, entityId: true },
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const fromSnapshot = allSnapshots.find(s => s.entityId === fromEntityId)
|
|
161
|
+
const toSnapshot = allSnapshots.find(s => s.entityId === referencedEntityId)
|
|
162
|
+
|
|
163
|
+
expect(fromSnapshot).toBeDefined()
|
|
164
|
+
expect(toSnapshot).toBeDefined()
|
|
165
|
+
|
|
166
|
+
const refs = await projectDatabase.entitySnapshotReference.findMany({
|
|
167
|
+
where: { fromId: fromSnapshot?.id },
|
|
168
|
+
orderBy: [{ toId: "asc" }, { kind: "asc" }, { group: "asc" }],
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
expect(refs).toEqual(
|
|
172
|
+
expect.arrayContaining([
|
|
173
|
+
expect.objectContaining({
|
|
174
|
+
fromId: fromSnapshot!.id,
|
|
175
|
+
toId: toSnapshot!.id,
|
|
176
|
+
kind: "inclusion",
|
|
177
|
+
group: "child",
|
|
178
|
+
}),
|
|
179
|
+
expect.objectContaining({
|
|
180
|
+
fromId: fromSnapshot!.id,
|
|
181
|
+
toId: toSnapshot!.id,
|
|
182
|
+
kind: "explicit",
|
|
183
|
+
group: "deps",
|
|
184
|
+
}),
|
|
185
|
+
]),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const uniqueEdges = new Set(refs.map(r => `${r.fromId}:${r.toId}:${r.kind}:${r.group}`))
|
|
189
|
+
expect(uniqueEdges.size).toBe(refs.length)
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
entitySnapshotTest(
|
|
194
|
+
"throws when explicit reference points to missing entity",
|
|
195
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
196
|
+
const operation = await createOperation(projectDatabase)
|
|
197
|
+
const state = await createInstanceState(project.id)
|
|
198
|
+
|
|
199
|
+
const fromEntityId = getEntityId({
|
|
200
|
+
$meta: { type: "from.v1", identity: "from-1" },
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
entitySnapshotService.persistUnitEntitySnapshots({
|
|
205
|
+
projectId: project.id,
|
|
206
|
+
operationId: operation.id,
|
|
207
|
+
stateId: state.id,
|
|
208
|
+
payload: {
|
|
209
|
+
nodes: [
|
|
210
|
+
{
|
|
211
|
+
entityId: fromEntityId,
|
|
212
|
+
entityType: "from.v1",
|
|
213
|
+
identity: "from-1",
|
|
214
|
+
meta: { title: "From" },
|
|
215
|
+
content: { v: 1 },
|
|
216
|
+
referencedOutputs: [],
|
|
217
|
+
exportedOutputs: ["out"],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
implicitReferences: [],
|
|
221
|
+
explicitReferences: [
|
|
222
|
+
{ fromEntityId: fromEntityId, toEntityId: "missing-entity", group: "deps" },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
).rejects.toThrow('Referenced entity "missing-entity" does not exist')
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
entitySnapshotTest(
|
|
231
|
+
"persists snapshot meta as null when not provided",
|
|
232
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
233
|
+
const operation = await createOperation(projectDatabase)
|
|
234
|
+
const state = await createInstanceState(project.id)
|
|
235
|
+
|
|
236
|
+
const entityId = getEntityId({
|
|
237
|
+
$meta: { type: "test.entity.v1", identity: "id-1" },
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
241
|
+
projectId: project.id,
|
|
242
|
+
operationId: operation.id,
|
|
243
|
+
stateId: state.id,
|
|
244
|
+
payload: {
|
|
245
|
+
nodes: [
|
|
246
|
+
{
|
|
247
|
+
entityId,
|
|
248
|
+
entityType: "test.entity.v1",
|
|
249
|
+
identity: "id-1",
|
|
250
|
+
meta: null,
|
|
251
|
+
content: { value: "hello" },
|
|
252
|
+
referencedOutputs: [],
|
|
253
|
+
exportedOutputs: ["value"],
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
implicitReferences: [],
|
|
257
|
+
explicitReferences: [],
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const snapshot = await projectDatabase.entitySnapshot.findFirst({
|
|
262
|
+
where: { operationId: operation.id, stateId: state.id, entityId },
|
|
263
|
+
select: {
|
|
264
|
+
content: {
|
|
265
|
+
select: { meta: true },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(snapshot?.content.meta).toBeNull()
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
describe("listReferencedEntitySnapshotsForOutput", () => {
|
|
276
|
+
entitySnapshotTest(
|
|
277
|
+
"returns snapshots when output is exported",
|
|
278
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
279
|
+
const operation = await createOperation(projectDatabase)
|
|
280
|
+
const state = await createInstanceState(project.id)
|
|
281
|
+
|
|
282
|
+
const entityId = getEntityId({
|
|
283
|
+
$meta: { type: "test.entity.v1", identity: "id-1" },
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
287
|
+
projectId: project.id,
|
|
288
|
+
operationId: operation.id,
|
|
289
|
+
stateId: state.id,
|
|
290
|
+
payload: {
|
|
291
|
+
nodes: [
|
|
292
|
+
{
|
|
293
|
+
entityId,
|
|
294
|
+
entityType: "test.entity.v1",
|
|
295
|
+
identity: "id-1",
|
|
296
|
+
meta: { title: "Exported" },
|
|
297
|
+
content: { value: "hello" },
|
|
298
|
+
referencedOutputs: [],
|
|
299
|
+
exportedOutputs: ["value"],
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
implicitReferences: [],
|
|
303
|
+
explicitReferences: [],
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const snapshots = await entitySnapshotService.listReferencedEntitySnapshotsForOutput(
|
|
308
|
+
project.id,
|
|
309
|
+
state.id,
|
|
310
|
+
"value",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(snapshots).toHaveLength(1)
|
|
314
|
+
expect(snapshots[0]).toEqual(
|
|
315
|
+
expect.objectContaining({
|
|
316
|
+
entityId,
|
|
317
|
+
entityType: "test.entity.v1",
|
|
318
|
+
entityIdentity: "id-1",
|
|
319
|
+
content: { value: "hello" },
|
|
320
|
+
}),
|
|
321
|
+
)
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
entitySnapshotTest(
|
|
326
|
+
"returns snapshots when output is only referenced",
|
|
327
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
328
|
+
const operation = await createOperation(projectDatabase)
|
|
329
|
+
const state = await createInstanceState(project.id)
|
|
330
|
+
|
|
331
|
+
const entityId = getEntityId({
|
|
332
|
+
$meta: { type: "test.entity.v1", identity: "id-1" },
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
336
|
+
projectId: project.id,
|
|
337
|
+
operationId: operation.id,
|
|
338
|
+
stateId: state.id,
|
|
339
|
+
payload: {
|
|
340
|
+
nodes: [
|
|
341
|
+
{
|
|
342
|
+
entityId,
|
|
343
|
+
entityType: "test.entity.v1",
|
|
344
|
+
identity: "id-1",
|
|
345
|
+
meta: { title: "Referenced" },
|
|
346
|
+
content: { value: "hello" },
|
|
347
|
+
referencedOutputs: ["value"],
|
|
348
|
+
exportedOutputs: [],
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
implicitReferences: [],
|
|
352
|
+
explicitReferences: [],
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const snapshots = await entitySnapshotService.listReferencedEntitySnapshotsForOutput(
|
|
357
|
+
project.id,
|
|
358
|
+
state.id,
|
|
359
|
+
"value",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
expect(snapshots).toHaveLength(1)
|
|
363
|
+
expect(snapshots[0]).toEqual(
|
|
364
|
+
expect.objectContaining({
|
|
365
|
+
entityId,
|
|
366
|
+
entityType: "test.entity.v1",
|
|
367
|
+
entityIdentity: "id-1",
|
|
368
|
+
content: { value: "hello" },
|
|
369
|
+
}),
|
|
370
|
+
)
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
entitySnapshotTest(
|
|
375
|
+
"can reconstruct snapshot values with $meta and inclusions",
|
|
376
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
377
|
+
const operation = await createOperation(projectDatabase)
|
|
378
|
+
const state = await createInstanceState(project.id)
|
|
379
|
+
|
|
380
|
+
const parentEntityId = getEntityId({
|
|
381
|
+
$meta: { type: "parent.v1", identity: "p1" },
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const childEntityId = getEntityId({
|
|
385
|
+
$meta: { type: "child.v1", identity: "c1" },
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
389
|
+
projectId: project.id,
|
|
390
|
+
operationId: operation.id,
|
|
391
|
+
stateId: state.id,
|
|
392
|
+
payload: {
|
|
393
|
+
nodes: [
|
|
394
|
+
{
|
|
395
|
+
entityId: parentEntityId,
|
|
396
|
+
entityType: "parent.v1",
|
|
397
|
+
identity: "p1",
|
|
398
|
+
meta: { title: "Parent" },
|
|
399
|
+
content: { value: "p" },
|
|
400
|
+
referencedOutputs: [],
|
|
401
|
+
exportedOutputs: ["out"],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
entityId: childEntityId,
|
|
405
|
+
entityType: "child.v1",
|
|
406
|
+
identity: "c1",
|
|
407
|
+
meta: { title: "Child" },
|
|
408
|
+
content: { value: "c" },
|
|
409
|
+
referencedOutputs: ["out"],
|
|
410
|
+
exportedOutputs: [],
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
implicitReferences: [
|
|
414
|
+
{
|
|
415
|
+
fromEntityId: parentEntityId,
|
|
416
|
+
toEntityId: childEntityId,
|
|
417
|
+
group: "child",
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
explicitReferences: [],
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const library = {
|
|
425
|
+
components: {},
|
|
426
|
+
entities: {
|
|
427
|
+
"parent.v1": {
|
|
428
|
+
type: "parent.v1",
|
|
429
|
+
inclusions: [
|
|
430
|
+
{
|
|
431
|
+
type: "child.v1",
|
|
432
|
+
field: "child",
|
|
433
|
+
required: false,
|
|
434
|
+
multiple: false,
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
"child.v1": { type: "child.v1", inclusions: [] },
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const snapshots = await entitySnapshotService.listReferencedEntitySnapshotsForOutput(
|
|
443
|
+
project.id,
|
|
444
|
+
state.id,
|
|
445
|
+
"out",
|
|
446
|
+
library as never,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
expect(snapshots).toHaveLength(2)
|
|
450
|
+
|
|
451
|
+
const parent = snapshots.find(s => s.entityId === parentEntityId)
|
|
452
|
+
expect(parent?.content).toMatchObject({
|
|
453
|
+
$meta: { type: "parent.v1", identity: "p1", title: "Parent" },
|
|
454
|
+
value: "p",
|
|
455
|
+
child: {
|
|
456
|
+
$meta: { type: "child.v1", identity: "c1", title: "Child" },
|
|
457
|
+
value: "c",
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
expect((parent?.content as { $meta?: unknown } | undefined)?.$meta).toMatchObject({
|
|
462
|
+
snapshotId: expect.any(String),
|
|
463
|
+
})
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
describe("reconstructLatestExportedOutputValues", () => {
|
|
469
|
+
entitySnapshotTest(
|
|
470
|
+
"returns most recent exported values and does not cross-product keys",
|
|
471
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
472
|
+
const stateA = await createInstanceState(project.id)
|
|
473
|
+
const stateB = await createInstanceState(project.id)
|
|
474
|
+
|
|
475
|
+
const op1 = await createOperation(projectDatabase)
|
|
476
|
+
const op2 = await createOperation(projectDatabase)
|
|
477
|
+
|
|
478
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
479
|
+
projectId: project.id,
|
|
480
|
+
operationId: op1.id,
|
|
481
|
+
stateId: stateA.id,
|
|
482
|
+
payload: {
|
|
483
|
+
nodes: [
|
|
484
|
+
{
|
|
485
|
+
entityId: getEntityId({ $meta: { type: "a.v1", identity: "a1" } }),
|
|
486
|
+
entityType: "a.v1",
|
|
487
|
+
identity: "a1",
|
|
488
|
+
meta: { title: "A/outA" },
|
|
489
|
+
content: { n: 1 },
|
|
490
|
+
referencedOutputs: [],
|
|
491
|
+
exportedOutputs: ["outA"],
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
entityId: getEntityId({ $meta: { type: "a.v1", identity: "a2" } }),
|
|
495
|
+
entityType: "a.v1",
|
|
496
|
+
identity: "a2",
|
|
497
|
+
meta: { title: "A/outB" },
|
|
498
|
+
content: { n: 2 },
|
|
499
|
+
referencedOutputs: [],
|
|
500
|
+
exportedOutputs: ["outB"],
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
implicitReferences: [],
|
|
504
|
+
explicitReferences: [],
|
|
505
|
+
},
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
509
|
+
projectId: project.id,
|
|
510
|
+
operationId: op1.id,
|
|
511
|
+
stateId: stateB.id,
|
|
512
|
+
payload: {
|
|
513
|
+
nodes: [
|
|
514
|
+
{
|
|
515
|
+
entityId: getEntityId({ $meta: { type: "b.v1", identity: "b1" } }),
|
|
516
|
+
entityType: "b.v1",
|
|
517
|
+
identity: "b1",
|
|
518
|
+
meta: { title: "B/outA" },
|
|
519
|
+
content: { n: 3 },
|
|
520
|
+
referencedOutputs: [],
|
|
521
|
+
exportedOutputs: ["outA"],
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
entityId: getEntityId({ $meta: { type: "b.v1", identity: "b2" } }),
|
|
525
|
+
entityType: "b.v1",
|
|
526
|
+
identity: "b2",
|
|
527
|
+
meta: { title: "B/outB" },
|
|
528
|
+
content: { n: 4 },
|
|
529
|
+
referencedOutputs: [],
|
|
530
|
+
exportedOutputs: ["outB"],
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
implicitReferences: [],
|
|
534
|
+
explicitReferences: [],
|
|
535
|
+
},
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
539
|
+
projectId: project.id,
|
|
540
|
+
operationId: op2.id,
|
|
541
|
+
stateId: stateA.id,
|
|
542
|
+
payload: {
|
|
543
|
+
nodes: [
|
|
544
|
+
{
|
|
545
|
+
entityId: getEntityId({ $meta: { type: "a.v1", identity: "a3" } }),
|
|
546
|
+
entityType: "a.v1",
|
|
547
|
+
identity: "a3",
|
|
548
|
+
meta: { title: "A/outA newer" },
|
|
549
|
+
content: { n: 10 },
|
|
550
|
+
referencedOutputs: [],
|
|
551
|
+
exportedOutputs: ["outA"],
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
implicitReferences: [],
|
|
555
|
+
explicitReferences: [],
|
|
556
|
+
},
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
const t1 = new Date("2020-01-01T00:00:00.000Z")
|
|
560
|
+
const t2 = new Date("2020-01-02T00:00:00.000Z")
|
|
561
|
+
|
|
562
|
+
const op1Snapshots = await projectDatabase.entitySnapshot.findMany({
|
|
563
|
+
where: { operationId: op1.id },
|
|
564
|
+
select: { id: true },
|
|
565
|
+
})
|
|
566
|
+
const op2Snapshots = await projectDatabase.entitySnapshot.findMany({
|
|
567
|
+
where: { operationId: op2.id },
|
|
568
|
+
select: { id: true },
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
for (const s of op1Snapshots) {
|
|
572
|
+
await projectDatabase.entitySnapshot.update({
|
|
573
|
+
where: { id: s.id },
|
|
574
|
+
data: { createdAt: t1 },
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (const s of op2Snapshots) {
|
|
579
|
+
await projectDatabase.entitySnapshot.update({
|
|
580
|
+
where: { id: s.id },
|
|
581
|
+
data: { createdAt: t2 },
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const library = {
|
|
586
|
+
components: {},
|
|
587
|
+
entities: {
|
|
588
|
+
"a.v1": { type: "a.v1", inclusions: [] },
|
|
589
|
+
"b.v1": { type: "b.v1", inclusions: [] },
|
|
590
|
+
},
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const captured = await entitySnapshotService.reconstructLatestExportedOutputValues(
|
|
594
|
+
project.id,
|
|
595
|
+
[
|
|
596
|
+
{ stateId: stateA.id, output: "outA" },
|
|
597
|
+
{ stateId: stateB.id, output: "outB" },
|
|
598
|
+
],
|
|
599
|
+
library as never,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
expect(Array.from(captured.keys()).sort()).toEqual(
|
|
603
|
+
[`${stateA.id}:outA`, `${stateB.id}:outB`].sort(),
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
const newerValues = captured.get(`${stateA.id}:outA`) ?? []
|
|
607
|
+
expect(newerValues).toHaveLength(1)
|
|
608
|
+
expect(newerValues[0]).toMatchObject({ ok: true })
|
|
609
|
+
if (newerValues[0]?.ok) {
|
|
610
|
+
expect(newerValues[0].value).toMatchObject({
|
|
611
|
+
$meta: { type: "a.v1", identity: "a3" },
|
|
612
|
+
n: 10,
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
expect(newerValues[0].value.$meta).not.toHaveProperty("snapshotId")
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const bOutB = captured.get(`${stateB.id}:outB`) ?? []
|
|
619
|
+
expect(bOutB).toHaveLength(1)
|
|
620
|
+
expect(bOutB[0]).toMatchObject({ ok: true })
|
|
621
|
+
if (bOutB[0]?.ok) {
|
|
622
|
+
expect(bOutB[0].value).toMatchObject({
|
|
623
|
+
$meta: { type: "b.v1", identity: "b2" },
|
|
624
|
+
n: 4,
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
expect(bOutB[0].value.$meta).not.toHaveProperty("snapshotId")
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
entitySnapshotTest(
|
|
633
|
+
"captures reconstruction errors (missing required inclusion) instead of throwing",
|
|
634
|
+
async ({ entitySnapshotService, projectDatabase, project, createInstanceState, expect }) => {
|
|
635
|
+
const operation = await createOperation(projectDatabase)
|
|
636
|
+
const state = await createInstanceState(project.id)
|
|
637
|
+
|
|
638
|
+
await entitySnapshotService.persistUnitEntitySnapshots({
|
|
639
|
+
projectId: project.id,
|
|
640
|
+
operationId: operation.id,
|
|
641
|
+
stateId: state.id,
|
|
642
|
+
payload: {
|
|
643
|
+
nodes: [
|
|
644
|
+
{
|
|
645
|
+
entityId: getEntityId({ $meta: { type: "parent.v1", identity: "p1" } }),
|
|
646
|
+
entityType: "parent.v1",
|
|
647
|
+
identity: "p1",
|
|
648
|
+
meta: { title: "Parent" },
|
|
649
|
+
content: { value: "p" },
|
|
650
|
+
referencedOutputs: [],
|
|
651
|
+
exportedOutputs: ["out"],
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
implicitReferences: [],
|
|
655
|
+
explicitReferences: [],
|
|
656
|
+
},
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
const library = {
|
|
660
|
+
components: {},
|
|
661
|
+
entities: {
|
|
662
|
+
"parent.v1": {
|
|
663
|
+
type: "parent.v1",
|
|
664
|
+
inclusions: [{ type: "child.v1", field: "child", required: true }],
|
|
665
|
+
},
|
|
666
|
+
"child.v1": { type: "child.v1", inclusions: [] },
|
|
667
|
+
},
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const captured = await entitySnapshotService.reconstructLatestExportedOutputValues(
|
|
671
|
+
project.id,
|
|
672
|
+
[{ stateId: state.id, output: "out", operationId: operation.id }],
|
|
673
|
+
library as never,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
const values = captured.get(`${state.id}:out`) ?? []
|
|
677
|
+
expect(values).toHaveLength(1)
|
|
678
|
+
expect(values[0]).toMatchObject({ ok: false })
|
|
679
|
+
if (values[0] && !values[0].ok) {
|
|
680
|
+
expect(values[0].error.message).toMatch(/missing required inclusion/i)
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
)
|
|
684
|
+
})
|