@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LibraryBackend, ProjectEvaluationResult } from "../library"
|
|
2
2
|
import type { PubSubManager } from "../pubsub"
|
|
3
3
|
import type { LibraryModel, ProjectModel } from "../shared"
|
|
4
|
+
import type { ObjectRefIndexService } from "./object-ref-index"
|
|
4
5
|
import type { ProjectModelService } from "./project-model"
|
|
5
6
|
import type { ProjectUnlockService } from "./project-unlock"
|
|
6
7
|
import { defineComponent, defineUnit, getInstanceId, type InstanceModel } from "@highstate/contract"
|
|
@@ -141,6 +142,9 @@ describe("ProjectEvaluationSubsystem", () => {
|
|
|
141
142
|
projectModelService,
|
|
142
143
|
pubsubManager,
|
|
143
144
|
projectUnlockService,
|
|
145
|
+
vi.mockObject({
|
|
146
|
+
track: vi.fn().mockResolvedValue(undefined),
|
|
147
|
+
} as unknown as ObjectRefIndexService),
|
|
144
148
|
logger,
|
|
145
149
|
)
|
|
146
150
|
|
|
@@ -317,4 +321,56 @@ describe("ProjectEvaluationSubsystem", () => {
|
|
|
317
321
|
expect(payload?.deletedGhostInstanceIds).toEqual([virtualInstance.id])
|
|
318
322
|
},
|
|
319
323
|
)
|
|
324
|
+
|
|
325
|
+
evaluationTest(
|
|
326
|
+
"deduplicates evaluation states and prioritizes errors",
|
|
327
|
+
async ({
|
|
328
|
+
subsystem,
|
|
329
|
+
project,
|
|
330
|
+
projectDatabase,
|
|
331
|
+
projectModelService,
|
|
332
|
+
libraryBackend,
|
|
333
|
+
expect,
|
|
334
|
+
}) => {
|
|
335
|
+
// arrange
|
|
336
|
+
const virtualInstance = createVirtualInstance("overlap")
|
|
337
|
+
|
|
338
|
+
const evaluationResult: ProjectEvaluationResult = {
|
|
339
|
+
success: true,
|
|
340
|
+
virtualInstances: [virtualInstance],
|
|
341
|
+
topLevelErrors: {
|
|
342
|
+
[virtualInstance.id]: "failed to evaluate subtree",
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
projectModelService.resolveProject.mockResolvedValue({
|
|
347
|
+
project,
|
|
348
|
+
library: clone(libraryModel),
|
|
349
|
+
instances: [
|
|
350
|
+
{
|
|
351
|
+
...virtualInstance,
|
|
352
|
+
kind: "composite",
|
|
353
|
+
type: "composite.v1",
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
stateMap: new Map(),
|
|
357
|
+
resolvedInputs: {},
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
libraryBackend.evaluateCompositeInstances.mockResolvedValue(evaluationResult)
|
|
361
|
+
|
|
362
|
+
// act
|
|
363
|
+
await subsystem.evaluateProject(project.id)
|
|
364
|
+
|
|
365
|
+
// assert
|
|
366
|
+
const state = await projectDatabase.instanceState.findUniqueOrThrow({
|
|
367
|
+
where: { instanceId: virtualInstance.id },
|
|
368
|
+
include: { evaluationState: true },
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
expect(state.evaluationState).toBeDefined()
|
|
372
|
+
expect(state.evaluationState?.status).toBe("error")
|
|
373
|
+
expect(state.evaluationState?.message).toBe("failed to evaluate subtree")
|
|
374
|
+
},
|
|
375
|
+
)
|
|
320
376
|
})
|
|
@@ -2,6 +2,7 @@ import type { InstanceId, InstanceModel } from "@highstate/contract"
|
|
|
2
2
|
import type { Logger } from "pino"
|
|
3
3
|
import type { LibraryBackend } from "../library"
|
|
4
4
|
import type { PubSubManager } from "../pubsub"
|
|
5
|
+
import type { ObjectRefIndexService } from "./object-ref-index"
|
|
5
6
|
import type { ProjectModelService } from "./project-model"
|
|
6
7
|
import type { ProjectUnlockService } from "./project-unlock"
|
|
7
8
|
import { isNonNullish } from "remeda"
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type InstanceEvaluationStateUpdateInput,
|
|
14
15
|
type InstanceStatus,
|
|
15
16
|
} from "../database"
|
|
17
|
+
import { stableJsonStringify } from "../shared"
|
|
16
18
|
|
|
17
19
|
type EvaluatedInstance = {
|
|
18
20
|
instanceId: InstanceId
|
|
@@ -23,6 +25,8 @@ type EvaluatedInstance = {
|
|
|
23
25
|
|
|
24
26
|
export class ProjectEvaluationSubsystem {
|
|
25
27
|
private readonly projectWatchers = new Map<string, AbortController>()
|
|
28
|
+
private readonly evaluationPromises = new Map<string, Promise<void>>()
|
|
29
|
+
private readonly evaluationRequested = new Set<string>()
|
|
26
30
|
|
|
27
31
|
constructor(
|
|
28
32
|
private readonly database: DatabaseManager,
|
|
@@ -30,6 +34,7 @@ export class ProjectEvaluationSubsystem {
|
|
|
30
34
|
private readonly projectModelService: ProjectModelService,
|
|
31
35
|
private readonly pubsubManager: PubSubManager,
|
|
32
36
|
private readonly projectUnlockService: ProjectUnlockService,
|
|
37
|
+
private readonly objectRefIndexService: ObjectRefIndexService,
|
|
33
38
|
private readonly logger: Logger,
|
|
34
39
|
) {
|
|
35
40
|
this.projectUnlockService.registerUnlockTask(
|
|
@@ -46,6 +51,30 @@ export class ProjectEvaluationSubsystem {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
async evaluateProject(projectId: string): Promise<void> {
|
|
54
|
+
const existingPromise = this.evaluationPromises.get(projectId)
|
|
55
|
+
if (existingPromise) {
|
|
56
|
+
this.evaluationRequested.add(projectId)
|
|
57
|
+
await existingPromise
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const promise = this.evaluateProjectLoop(projectId).finally(() => {
|
|
62
|
+
this.evaluationPromises.delete(projectId)
|
|
63
|
+
this.evaluationRequested.delete(projectId)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
this.evaluationPromises.set(projectId, promise)
|
|
67
|
+
await promise
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async evaluateProjectLoop(projectId: string): Promise<void> {
|
|
71
|
+
do {
|
|
72
|
+
this.evaluationRequested.delete(projectId)
|
|
73
|
+
await this.evaluateProjectNow(projectId)
|
|
74
|
+
} while (this.evaluationRequested.has(projectId))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async evaluateProjectNow(projectId: string): Promise<void> {
|
|
49
78
|
const { project, instances, resolvedInputs } =
|
|
50
79
|
await this.projectModelService.resolveProject(projectId)
|
|
51
80
|
|
|
@@ -88,10 +117,13 @@ export class ProjectEvaluationSubsystem {
|
|
|
88
117
|
model: null,
|
|
89
118
|
}))
|
|
90
119
|
|
|
91
|
-
await this.setInstanceEvaluationStates(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
await this.setInstanceEvaluationStates(
|
|
121
|
+
project.id,
|
|
122
|
+
ProjectEvaluationSubsystem.mergeEvaluatedInstances(
|
|
123
|
+
evaluatedInstances,
|
|
124
|
+
errorEvaluatedInstances,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
95
127
|
} else {
|
|
96
128
|
// set all composite instances to error state in evaluation
|
|
97
129
|
const errorEvaluationStates: EvaluatedInstance[] = compositeInstanceIds.map(instanceId => ({
|
|
@@ -135,7 +167,7 @@ export class ProjectEvaluationSubsystem {
|
|
|
135
167
|
): Promise<void> {
|
|
136
168
|
const database = await this.database.forProject(projectId)
|
|
137
169
|
|
|
138
|
-
await database.$transaction(async tx => {
|
|
170
|
+
const { stateIds } = await database.$transaction(async tx => {
|
|
139
171
|
const previousVirtualStates = await tx.instanceState.findMany({
|
|
140
172
|
where: { source: "virtual" },
|
|
141
173
|
select: {
|
|
@@ -186,6 +218,9 @@ export class ProjectEvaluationSubsystem {
|
|
|
186
218
|
const existingStates = await tx.instanceEvaluationState.findMany({
|
|
187
219
|
select: {
|
|
188
220
|
stateId: true,
|
|
221
|
+
status: true,
|
|
222
|
+
message: true,
|
|
223
|
+
model: true,
|
|
189
224
|
state: {
|
|
190
225
|
select: {
|
|
191
226
|
instanceId: true,
|
|
@@ -197,9 +232,21 @@ export class ProjectEvaluationSubsystem {
|
|
|
197
232
|
|
|
198
233
|
const existingStateIds = new Set(existingStates.map(state => state.stateId))
|
|
199
234
|
const actualStateIds = new Set(states.map(state => state.stateId))
|
|
235
|
+
const existingStateById = new Map(existingStates.map(state => [state.stateId, state]))
|
|
200
236
|
|
|
201
237
|
const newStates = states.filter(state => !existingStateIds.has(state.stateId))
|
|
202
|
-
const statesToUpdate = states.filter(state =>
|
|
238
|
+
const statesToUpdate = states.filter(state => {
|
|
239
|
+
const existingState = existingStateById.get(state.stateId)
|
|
240
|
+
if (!existingState) {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
existingState.status !== state.status ||
|
|
246
|
+
(existingState.message ?? null) !== (state.message ?? null) ||
|
|
247
|
+
stableJsonStringify(existingState.model) !== stableJsonStringify(state.model)
|
|
248
|
+
)
|
|
249
|
+
})
|
|
203
250
|
const statesToDelete = existingStates.filter(state => !actualStateIds.has(state.stateId))
|
|
204
251
|
|
|
205
252
|
// create new states
|
|
@@ -225,13 +272,15 @@ export class ProjectEvaluationSubsystem {
|
|
|
225
272
|
})
|
|
226
273
|
|
|
227
274
|
// 5. publish evaluation state updates
|
|
228
|
-
|
|
275
|
+
if (updatedStates.length > 0) {
|
|
229
276
|
void this.pubsubManager.publish(["instance-state", projectId], {
|
|
230
|
-
type: "patched",
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
277
|
+
type: "patched-batch",
|
|
278
|
+
patches: updatedStates.map(state => ({
|
|
279
|
+
stateId: state.stateId,
|
|
280
|
+
patch: {
|
|
281
|
+
evaluationState: state,
|
|
282
|
+
},
|
|
283
|
+
})),
|
|
235
284
|
})
|
|
236
285
|
}
|
|
237
286
|
|
|
@@ -276,19 +325,33 @@ export class ProjectEvaluationSubsystem {
|
|
|
276
325
|
}
|
|
277
326
|
}
|
|
278
327
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
deletedVirtualInstanceIds: statesToDelete.map(state => state.state.instanceId),
|
|
328
|
+
const updatedVirtualInstances = [...newStates, ...updatedStates]
|
|
329
|
+
.map(state => state.model as InstanceModel)
|
|
330
|
+
.filter(isNonNullish)
|
|
331
|
+
const deletedVirtualInstanceIds = statesToDelete.map(state => state.state.instanceId)
|
|
332
|
+
const updatedGhostInstances = newGhostInstances.filter(isNonNullish)
|
|
286
333
|
|
|
287
|
-
|
|
334
|
+
// 6. publish project model update
|
|
335
|
+
if (
|
|
336
|
+
updatedVirtualInstances.length > 0 ||
|
|
337
|
+
deletedVirtualInstanceIds.length > 0 ||
|
|
338
|
+
updatedGhostInstances.length > 0 ||
|
|
339
|
+
resolvedGhostInstanceIds.length > 0
|
|
340
|
+
) {
|
|
341
|
+
void this.pubsubManager.publish(["project-model", projectId], {
|
|
342
|
+
updatedVirtualInstances,
|
|
343
|
+
deletedVirtualInstanceIds,
|
|
344
|
+
updatedGhostInstances,
|
|
345
|
+
deletedGhostInstanceIds: resolvedGhostInstanceIds,
|
|
346
|
+
})
|
|
347
|
+
}
|
|
288
348
|
|
|
289
|
-
|
|
290
|
-
})
|
|
349
|
+
return { stateIds: Array.from(instanceIdToStateMap.values()).map(state => state.id) }
|
|
291
350
|
})
|
|
351
|
+
|
|
352
|
+
if (stateIds.length > 0) {
|
|
353
|
+
await this.objectRefIndexService.track(projectId, stateIds)
|
|
354
|
+
}
|
|
292
355
|
}
|
|
293
356
|
|
|
294
357
|
private async trackUnlockedProject(projectId: string): Promise<void> {
|
|
@@ -384,4 +447,21 @@ export class ProjectEvaluationSubsystem {
|
|
|
384
447
|
|
|
385
448
|
return `Composite instance evaluation completed successfully.\n\nInstance Tree:\n${tree}`
|
|
386
449
|
}
|
|
450
|
+
|
|
451
|
+
private static mergeEvaluatedInstances(
|
|
452
|
+
evaluatedInstances: EvaluatedInstance[],
|
|
453
|
+
errorEvaluatedInstances: EvaluatedInstance[],
|
|
454
|
+
): EvaluatedInstance[] {
|
|
455
|
+
const merged = new Map<InstanceId, EvaluatedInstance>()
|
|
456
|
+
|
|
457
|
+
for (const state of evaluatedInstances) {
|
|
458
|
+
merged.set(state.instanceId, state)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const state of errorEvaluatedInstances) {
|
|
462
|
+
merged.set(state.instanceId, state)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return Array.from(merged.values())
|
|
466
|
+
}
|
|
387
467
|
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import type { ProjectUnlockBackend } from "../unlock"
|
|
2
|
+
import { createId } from "@paralleldrive/cuid2"
|
|
3
|
+
import { describe, type MockedObject, vi } from "vitest"
|
|
4
|
+
import { test } from "../test-utils"
|
|
5
|
+
import { GlobalSearchService } from "./global-search"
|
|
6
|
+
|
|
7
|
+
const globalSearchTest = test.extend<{
|
|
8
|
+
projectUnlockBackend: MockedObject<ProjectUnlockBackend>
|
|
9
|
+
globalSearchService: GlobalSearchService
|
|
10
|
+
}>({
|
|
11
|
+
projectUnlockBackend: async ({}, use) => {
|
|
12
|
+
const projectUnlockBackend = vi.mockObject({
|
|
13
|
+
checkProjectUnlocked: vi.fn().mockResolvedValue(false),
|
|
14
|
+
getProjectMasterKey: vi.fn().mockResolvedValue(null),
|
|
15
|
+
unlockProject: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
lockProject: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
} as unknown as ProjectUnlockBackend)
|
|
18
|
+
|
|
19
|
+
await use(projectUnlockBackend)
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
globalSearchService: async ({ database, projectUnlockBackend, logger }, use) => {
|
|
23
|
+
const service = new GlobalSearchService(
|
|
24
|
+
database,
|
|
25
|
+
projectUnlockBackend,
|
|
26
|
+
logger.child({ service: "GlobalSearchService" }),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
await use(service)
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("searchByText", () => {
|
|
34
|
+
globalSearchTest(
|
|
35
|
+
"returns hits only from unlocked projects",
|
|
36
|
+
async ({
|
|
37
|
+
database,
|
|
38
|
+
createProject,
|
|
39
|
+
project,
|
|
40
|
+
projectUnlockBackend,
|
|
41
|
+
globalSearchService,
|
|
42
|
+
expect,
|
|
43
|
+
}) => {
|
|
44
|
+
// arrange
|
|
45
|
+
const lockedProject = await createProject("locked")
|
|
46
|
+
|
|
47
|
+
projectUnlockBackend.checkProjectUnlocked.mockImplementation(async projectId => {
|
|
48
|
+
return projectId === project.id
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const unlockedDb = await database.forProject(project.id)
|
|
52
|
+
const lockedDb = await database.forProject(lockedProject.id)
|
|
53
|
+
|
|
54
|
+
const unlockedOperationId = createId()
|
|
55
|
+
await unlockedDb.operation.create({
|
|
56
|
+
data: {
|
|
57
|
+
id: unlockedOperationId,
|
|
58
|
+
meta: { title: "Deploy application" },
|
|
59
|
+
type: "update",
|
|
60
|
+
options: {},
|
|
61
|
+
requestedInstanceIds: [],
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const lockedOperationId = createId()
|
|
66
|
+
await lockedDb.operation.create({
|
|
67
|
+
data: {
|
|
68
|
+
id: lockedOperationId,
|
|
69
|
+
meta: { title: "Deploy locked project" },
|
|
70
|
+
type: "update",
|
|
71
|
+
options: {},
|
|
72
|
+
requestedInstanceIds: [],
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// act
|
|
77
|
+
const result = await globalSearchService.searchByText("deploy")
|
|
78
|
+
|
|
79
|
+
// assert
|
|
80
|
+
expect(result.projects).toHaveLength(1)
|
|
81
|
+
expect(result.projects[0].projectId).toBe(project.id)
|
|
82
|
+
|
|
83
|
+
const operationHits = result.projects[0].hits.filter(h => h.kind === "operation")
|
|
84
|
+
expect(operationHits.map(h => h.id)).toEqual([unlockedOperationId])
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
globalSearchTest(
|
|
89
|
+
"matches instance states by instanceId",
|
|
90
|
+
async ({ database, project, projectUnlockBackend, globalSearchService, expect }) => {
|
|
91
|
+
// arrange
|
|
92
|
+
projectUnlockBackend.checkProjectUnlocked.mockResolvedValue(true)
|
|
93
|
+
|
|
94
|
+
const db = await database.forProject(project.id)
|
|
95
|
+
|
|
96
|
+
const stateId = createId()
|
|
97
|
+
const instanceId = "component.v1:my-instance-123"
|
|
98
|
+
|
|
99
|
+
await db.instanceState.create({
|
|
100
|
+
data: {
|
|
101
|
+
id: stateId,
|
|
102
|
+
instanceId,
|
|
103
|
+
status: "undeployed",
|
|
104
|
+
source: "resident",
|
|
105
|
+
kind: "unit",
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// act
|
|
110
|
+
const result = await globalSearchService.searchByText("my-instance")
|
|
111
|
+
|
|
112
|
+
// assert
|
|
113
|
+
expect(result.projects).toHaveLength(1)
|
|
114
|
+
expect(result.projects[0].projectId).toBe(project.id)
|
|
115
|
+
|
|
116
|
+
const hits = result.projects[0].hits.filter(h => h.kind === "instanceState")
|
|
117
|
+
expect(hits).toHaveLength(1)
|
|
118
|
+
expect(hits[0].id).toBe(stateId)
|
|
119
|
+
expect(hits[0].meta.title).toBe(instanceId)
|
|
120
|
+
expect(hits[0].meta.description).toBe("undeployed")
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
globalSearchTest(
|
|
125
|
+
"returns empty result for empty query",
|
|
126
|
+
async ({ projectUnlockBackend, globalSearchService, expect }) => {
|
|
127
|
+
// arrange
|
|
128
|
+
projectUnlockBackend.checkProjectUnlocked.mockResolvedValue(true)
|
|
129
|
+
|
|
130
|
+
// act
|
|
131
|
+
const result = await globalSearchService.searchByText(" ")
|
|
132
|
+
|
|
133
|
+
// assert
|
|
134
|
+
expect(result.projects).toEqual([])
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("searchByIds", () => {
|
|
140
|
+
globalSearchTest(
|
|
141
|
+
"returns empty result for empty ids",
|
|
142
|
+
async ({ globalSearchService, expect }) => {
|
|
143
|
+
// act
|
|
144
|
+
const result = await globalSearchService.searchByIds([])
|
|
145
|
+
|
|
146
|
+
// assert
|
|
147
|
+
expect(result).toEqual([])
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
globalSearchTest(
|
|
152
|
+
"marks projects as locked when the project is locked",
|
|
153
|
+
async ({ database, project, projectUnlockBackend, globalSearchService, expect }) => {
|
|
154
|
+
// arrange
|
|
155
|
+
projectUnlockBackend.checkProjectUnlocked.mockResolvedValue(false)
|
|
156
|
+
|
|
157
|
+
const objectId = createId()
|
|
158
|
+
await database.backend.object.create({
|
|
159
|
+
data: {
|
|
160
|
+
id: objectId,
|
|
161
|
+
projectId: project.id,
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// act
|
|
166
|
+
const result = await globalSearchService.searchByIds([objectId])
|
|
167
|
+
|
|
168
|
+
// assert
|
|
169
|
+
expect(result).toHaveLength(1)
|
|
170
|
+
expect(result[0].id).toBe(objectId)
|
|
171
|
+
expect(result[0].projects).toEqual([{ projectId: project.id, unlockState: "locked" }])
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
globalSearchTest(
|
|
176
|
+
"returns hits for unlocked projects and does not leak details for locked ones",
|
|
177
|
+
async ({
|
|
178
|
+
database,
|
|
179
|
+
createProject,
|
|
180
|
+
project,
|
|
181
|
+
projectUnlockBackend,
|
|
182
|
+
globalSearchService,
|
|
183
|
+
expect,
|
|
184
|
+
}) => {
|
|
185
|
+
// arrange
|
|
186
|
+
const lockedProject = await createProject(`locked-ids-${createId()}`)
|
|
187
|
+
|
|
188
|
+
projectUnlockBackend.checkProjectUnlocked.mockImplementation(async projectId => {
|
|
189
|
+
return projectId === project.id
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const objectId = createId()
|
|
193
|
+
await database.backend.object.createMany({
|
|
194
|
+
data: [
|
|
195
|
+
{ id: objectId, projectId: project.id },
|
|
196
|
+
{ id: objectId, projectId: lockedProject.id },
|
|
197
|
+
],
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const unlockedDb = await database.forProject(project.id)
|
|
201
|
+
await unlockedDb.operation.create({
|
|
202
|
+
data: {
|
|
203
|
+
id: objectId,
|
|
204
|
+
meta: { title: "Deploy application" },
|
|
205
|
+
type: "update",
|
|
206
|
+
options: {},
|
|
207
|
+
requestedInstanceIds: [],
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// act
|
|
212
|
+
const result = await globalSearchService.searchByIds([objectId])
|
|
213
|
+
|
|
214
|
+
// assert
|
|
215
|
+
expect(result).toHaveLength(1)
|
|
216
|
+
|
|
217
|
+
const projectResults = result[0].projects
|
|
218
|
+
expect(projectResults).toHaveLength(2)
|
|
219
|
+
|
|
220
|
+
const unlocked = projectResults.find(p => p.projectId === project.id)
|
|
221
|
+
expect(unlocked).toEqual({
|
|
222
|
+
projectId: project.id,
|
|
223
|
+
unlockState: "unlocked",
|
|
224
|
+
hits: [
|
|
225
|
+
{
|
|
226
|
+
kind: "operation",
|
|
227
|
+
id: objectId,
|
|
228
|
+
meta: { title: "Deploy application" },
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const locked = projectResults.find(p => p.projectId === lockedProject.id)
|
|
234
|
+
expect(locked).toEqual({ projectId: lockedProject.id, unlockState: "locked" })
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
globalSearchTest(
|
|
239
|
+
"returns unlocked projects with empty hits when the indexed object is not found in curated collections",
|
|
240
|
+
async ({ database, project, projectUnlockBackend, globalSearchService, expect }) => {
|
|
241
|
+
// arrange
|
|
242
|
+
projectUnlockBackend.checkProjectUnlocked.mockResolvedValue(true)
|
|
243
|
+
|
|
244
|
+
const objectId = createId()
|
|
245
|
+
await database.backend.object.create({
|
|
246
|
+
data: {
|
|
247
|
+
id: objectId,
|
|
248
|
+
projectId: project.id,
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// act
|
|
253
|
+
const result = await globalSearchService.searchByIds([objectId])
|
|
254
|
+
|
|
255
|
+
// assert
|
|
256
|
+
expect(result).toHaveLength(1)
|
|
257
|
+
expect(result[0].projects).toEqual([
|
|
258
|
+
{
|
|
259
|
+
projectId: project.id,
|
|
260
|
+
unlockState: "unlocked",
|
|
261
|
+
hits: [],
|
|
262
|
+
},
|
|
263
|
+
])
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
globalSearchTest(
|
|
268
|
+
"returns unlocked projects with empty hits when project database access fails",
|
|
269
|
+
async ({ database, createProject, projectUnlockBackend, logger, expect }) => {
|
|
270
|
+
// arrange
|
|
271
|
+
const projectA = await createProject(`aa-${createId()}`)
|
|
272
|
+
const projectB = await createProject(`bb-${createId()}`)
|
|
273
|
+
|
|
274
|
+
projectUnlockBackend.checkProjectUnlocked.mockResolvedValue(true)
|
|
275
|
+
|
|
276
|
+
const objectId = createId()
|
|
277
|
+
await database.backend.object.createMany({
|
|
278
|
+
data: [
|
|
279
|
+
{ id: objectId, projectId: projectA.id },
|
|
280
|
+
{ id: objectId, projectId: projectB.id },
|
|
281
|
+
],
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const unlockedDb = await database.forProject(projectA.id)
|
|
285
|
+
await unlockedDb.operation.create({
|
|
286
|
+
data: {
|
|
287
|
+
id: objectId,
|
|
288
|
+
meta: { title: "Deploy application" },
|
|
289
|
+
type: "update",
|
|
290
|
+
options: {},
|
|
291
|
+
requestedInstanceIds: [],
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const failingDatabase = {
|
|
296
|
+
backend: database.backend,
|
|
297
|
+
isEncryptionEnabled: database.isEncryptionEnabled,
|
|
298
|
+
updateBackendUnlockRecipients: async () => {},
|
|
299
|
+
getProjectMasterKey: async () => undefined,
|
|
300
|
+
setupDatabase: async () => {
|
|
301
|
+
throw new Error("not implemented")
|
|
302
|
+
},
|
|
303
|
+
forProject: async (projectId: string) => {
|
|
304
|
+
if (projectId === projectB.id) {
|
|
305
|
+
throw new Error("boom")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return await database.forProject(projectId)
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const service = new GlobalSearchService(
|
|
313
|
+
failingDatabase as never,
|
|
314
|
+
projectUnlockBackend,
|
|
315
|
+
logger.child({ service: "GlobalSearchService" }),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
// act
|
|
319
|
+
const result = await service.searchByIds([objectId])
|
|
320
|
+
|
|
321
|
+
// assert
|
|
322
|
+
expect(result).toHaveLength(1)
|
|
323
|
+
|
|
324
|
+
const byProjectId = new Map(result[0].projects.map(p => [p.projectId, p]))
|
|
325
|
+
expect(byProjectId.get(projectA.id)).toEqual({
|
|
326
|
+
projectId: projectA.id,
|
|
327
|
+
unlockState: "unlocked",
|
|
328
|
+
hits: [
|
|
329
|
+
{
|
|
330
|
+
kind: "operation",
|
|
331
|
+
id: objectId,
|
|
332
|
+
meta: { title: "Deploy application" },
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
expect(byProjectId.get(projectB.id)).toEqual({
|
|
338
|
+
projectId: projectB.id,
|
|
339
|
+
unlockState: "unlocked",
|
|
340
|
+
hits: [],
|
|
341
|
+
})
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
})
|