@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,902 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import type { DatabaseManager } from "../database"
|
|
3
|
+
import type { ProjectDatabase } from "../database/prisma"
|
|
4
|
+
import type { ProjectUnlockBackend } from "../unlock"
|
|
5
|
+
import { type CommonObjectMeta, commonObjectMetaSchema } from "@highstate/contract"
|
|
6
|
+
|
|
7
|
+
export type GlobalSearchObjectKind =
|
|
8
|
+
| "apiKey"
|
|
9
|
+
| "artifact"
|
|
10
|
+
| "entity"
|
|
11
|
+
| "entitySnapshot"
|
|
12
|
+
| "instanceState"
|
|
13
|
+
| "operation"
|
|
14
|
+
| "page"
|
|
15
|
+
| "secret"
|
|
16
|
+
| "serviceAccount"
|
|
17
|
+
| "terminal"
|
|
18
|
+
| "terminalSession"
|
|
19
|
+
| "trigger"
|
|
20
|
+
| "unlockMethod"
|
|
21
|
+
| "worker"
|
|
22
|
+
| "workerVersion"
|
|
23
|
+
|
|
24
|
+
export type GlobalSearchHit = {
|
|
25
|
+
kind: GlobalSearchObjectKind
|
|
26
|
+
id: string
|
|
27
|
+
meta: CommonObjectMeta
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type GlobalSearchTextProjectResult = {
|
|
31
|
+
projectId: string
|
|
32
|
+
hits: GlobalSearchHit[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type GlobalSearchTextResult = {
|
|
36
|
+
text: string
|
|
37
|
+
projects: GlobalSearchTextProjectResult[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeSearchText(text: string): string {
|
|
41
|
+
return text.trim().toLowerCase()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stringIncludesQuery(value: string | undefined, query: string): boolean {
|
|
45
|
+
if (!value) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return value.toLowerCase().includes(query)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function matchesCommonObjectMeta(meta: unknown, query: string): boolean {
|
|
53
|
+
const parsed = commonObjectMetaSchema.safeParse(meta)
|
|
54
|
+
|
|
55
|
+
if (parsed.success) {
|
|
56
|
+
return (
|
|
57
|
+
stringIncludesQuery(parsed.data.title, query) ||
|
|
58
|
+
stringIncludesQuery(parsed.data.description, query)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (meta && typeof meta === "object") {
|
|
63
|
+
const title = "title" in meta && typeof meta.title === "string" ? meta.title : undefined
|
|
64
|
+
const description =
|
|
65
|
+
"description" in meta && typeof meta.description === "string" ? meta.description : undefined
|
|
66
|
+
|
|
67
|
+
return stringIncludesQuery(title, query) || stringIncludesQuery(description, query)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createSearchHits<TItem extends { id: string }>(
|
|
74
|
+
kind: GlobalSearchObjectKind,
|
|
75
|
+
items: TItem[],
|
|
76
|
+
metaExtractor: (item: TItem) => unknown,
|
|
77
|
+
logger: Logger,
|
|
78
|
+
): GlobalSearchHit[] {
|
|
79
|
+
return items.map(item => {
|
|
80
|
+
const metaResult = commonObjectMetaSchema.safeParse(metaExtractor(item))
|
|
81
|
+
|
|
82
|
+
if (!metaResult.success) {
|
|
83
|
+
logger.warn(
|
|
84
|
+
{ itemId: item.id, kind, error: metaResult.error },
|
|
85
|
+
"failed to parse meta for search hit, using fallback",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
kind,
|
|
90
|
+
id: item.id,
|
|
91
|
+
meta: {
|
|
92
|
+
title: item.id,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
kind,
|
|
99
|
+
id: item.id,
|
|
100
|
+
meta: metaResult.data,
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type GlobalSearchProjectResult =
|
|
106
|
+
| {
|
|
107
|
+
projectId: string
|
|
108
|
+
unlockState: "locked"
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
projectId: string
|
|
112
|
+
unlockState: "unlocked"
|
|
113
|
+
hits: GlobalSearchHit[]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type GlobalSearchResult = {
|
|
117
|
+
id: string
|
|
118
|
+
projects: GlobalSearchProjectResult[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type ProjectSearchResolver = (
|
|
122
|
+
database: ProjectDatabase,
|
|
123
|
+
ids: string[],
|
|
124
|
+
logger: Logger,
|
|
125
|
+
) => Promise<GlobalSearchHit[]>
|
|
126
|
+
|
|
127
|
+
const projectSearchResolvers: ProjectSearchResolver[] = [
|
|
128
|
+
async (database, ids, logger) => {
|
|
129
|
+
const operations = await database.operation.findMany({
|
|
130
|
+
where: {
|
|
131
|
+
id: {
|
|
132
|
+
in: ids,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
select: {
|
|
136
|
+
id: true,
|
|
137
|
+
meta: true,
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return createSearchHits("operation", operations, operation => operation.meta, logger)
|
|
142
|
+
},
|
|
143
|
+
async (database, ids, logger) => {
|
|
144
|
+
const states = await database.instanceState.findMany({
|
|
145
|
+
where: {
|
|
146
|
+
id: {
|
|
147
|
+
in: ids,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
select: {
|
|
151
|
+
id: true,
|
|
152
|
+
instanceId: true,
|
|
153
|
+
status: true,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return createSearchHits(
|
|
158
|
+
"instanceState",
|
|
159
|
+
states,
|
|
160
|
+
state => ({
|
|
161
|
+
title: state.instanceId,
|
|
162
|
+
description: `${state.status}`,
|
|
163
|
+
}),
|
|
164
|
+
logger,
|
|
165
|
+
)
|
|
166
|
+
},
|
|
167
|
+
async (database, ids, logger) => {
|
|
168
|
+
const artifacts = await database.artifact.findMany({
|
|
169
|
+
where: {
|
|
170
|
+
id: {
|
|
171
|
+
in: ids,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
select: {
|
|
175
|
+
id: true,
|
|
176
|
+
meta: true,
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return createSearchHits("artifact", artifacts, artifact => artifact.meta, logger)
|
|
181
|
+
},
|
|
182
|
+
async (database, ids, logger) => {
|
|
183
|
+
const pages = await database.page.findMany({
|
|
184
|
+
where: {
|
|
185
|
+
id: {
|
|
186
|
+
in: ids,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
select: {
|
|
190
|
+
id: true,
|
|
191
|
+
meta: true,
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return createSearchHits("page", pages, page => page.meta, logger)
|
|
196
|
+
},
|
|
197
|
+
async (database, ids, logger) => {
|
|
198
|
+
const terminals = await database.terminal.findMany({
|
|
199
|
+
where: {
|
|
200
|
+
id: {
|
|
201
|
+
in: ids,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
select: {
|
|
205
|
+
id: true,
|
|
206
|
+
meta: true,
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
return createSearchHits("terminal", terminals, terminal => terminal.meta, logger)
|
|
211
|
+
},
|
|
212
|
+
async (database, ids, logger) => {
|
|
213
|
+
const sessions = await database.terminalSession.findMany({
|
|
214
|
+
where: {
|
|
215
|
+
id: {
|
|
216
|
+
in: ids,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
select: {
|
|
220
|
+
id: true,
|
|
221
|
+
terminal: {
|
|
222
|
+
select: {
|
|
223
|
+
meta: true,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
return createSearchHits("terminalSession", sessions, session => session.terminal.meta, logger)
|
|
230
|
+
},
|
|
231
|
+
async (database, ids, logger) => {
|
|
232
|
+
const secrets = await database.secret.findMany({
|
|
233
|
+
where: {
|
|
234
|
+
id: {
|
|
235
|
+
in: ids,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
select: {
|
|
239
|
+
id: true,
|
|
240
|
+
meta: true,
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
return createSearchHits("secret", secrets, secret => secret.meta, logger)
|
|
245
|
+
},
|
|
246
|
+
async (database, ids, logger) => {
|
|
247
|
+
const accounts = await database.serviceAccount.findMany({
|
|
248
|
+
where: {
|
|
249
|
+
id: {
|
|
250
|
+
in: ids,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
select: {
|
|
254
|
+
id: true,
|
|
255
|
+
meta: true,
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return createSearchHits("serviceAccount", accounts, account => account.meta, logger)
|
|
260
|
+
},
|
|
261
|
+
async (database, ids, logger) => {
|
|
262
|
+
const apiKeys = await database.apiKey.findMany({
|
|
263
|
+
where: {
|
|
264
|
+
id: {
|
|
265
|
+
in: ids,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
select: {
|
|
269
|
+
id: true,
|
|
270
|
+
meta: true,
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
return createSearchHits("apiKey", apiKeys, apiKey => apiKey.meta, logger)
|
|
275
|
+
},
|
|
276
|
+
async (database, ids, logger) => {
|
|
277
|
+
const triggers = await database.trigger.findMany({
|
|
278
|
+
where: {
|
|
279
|
+
id: {
|
|
280
|
+
in: ids,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
select: {
|
|
284
|
+
id: true,
|
|
285
|
+
meta: true,
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
return createSearchHits("trigger", triggers, trigger => trigger.meta, logger)
|
|
290
|
+
},
|
|
291
|
+
async (database, ids, logger) => {
|
|
292
|
+
const unlockMethods = await database.unlockMethod.findMany({
|
|
293
|
+
where: {
|
|
294
|
+
id: {
|
|
295
|
+
in: ids,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
select: {
|
|
299
|
+
id: true,
|
|
300
|
+
meta: true,
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
return createSearchHits(
|
|
305
|
+
"unlockMethod",
|
|
306
|
+
unlockMethods,
|
|
307
|
+
unlockMethod => unlockMethod.meta,
|
|
308
|
+
logger,
|
|
309
|
+
)
|
|
310
|
+
},
|
|
311
|
+
async (database, ids, logger) => {
|
|
312
|
+
const workers = await database.worker.findMany({
|
|
313
|
+
where: {
|
|
314
|
+
id: {
|
|
315
|
+
in: ids,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
select: {
|
|
319
|
+
id: true,
|
|
320
|
+
identity: true,
|
|
321
|
+
versions: {
|
|
322
|
+
orderBy: {
|
|
323
|
+
createdAt: "desc",
|
|
324
|
+
},
|
|
325
|
+
take: 1,
|
|
326
|
+
select: {
|
|
327
|
+
meta: true,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
return createSearchHits("worker", workers, worker => worker.versions[0]?.meta, logger)
|
|
334
|
+
},
|
|
335
|
+
async (database, ids, logger) => {
|
|
336
|
+
const versions = await database.workerVersion.findMany({
|
|
337
|
+
where: {
|
|
338
|
+
id: {
|
|
339
|
+
in: ids,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
select: {
|
|
343
|
+
id: true,
|
|
344
|
+
meta: true,
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return createSearchHits("workerVersion", versions, version => version.meta, logger)
|
|
349
|
+
},
|
|
350
|
+
async (database, ids, logger) => {
|
|
351
|
+
const snapshots = await database.entitySnapshot.findMany({
|
|
352
|
+
where: {
|
|
353
|
+
id: {
|
|
354
|
+
in: ids,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
select: {
|
|
358
|
+
id: true,
|
|
359
|
+
content: {
|
|
360
|
+
select: {
|
|
361
|
+
meta: true,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return createSearchHits("entitySnapshot", snapshots, snapshot => snapshot.content.meta, logger)
|
|
368
|
+
},
|
|
369
|
+
async (database, ids, logger) => {
|
|
370
|
+
const entities = await database.entity.findMany({
|
|
371
|
+
where: {
|
|
372
|
+
id: {
|
|
373
|
+
in: ids,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
select: {
|
|
377
|
+
id: true,
|
|
378
|
+
type: true,
|
|
379
|
+
identity: true,
|
|
380
|
+
snapshots: {
|
|
381
|
+
orderBy: {
|
|
382
|
+
createdAt: "desc",
|
|
383
|
+
},
|
|
384
|
+
take: 1,
|
|
385
|
+
select: {
|
|
386
|
+
content: {
|
|
387
|
+
select: {
|
|
388
|
+
meta: true,
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
return createSearchHits("entity", entities, entity => entity.snapshots[0]?.content.meta, logger)
|
|
397
|
+
},
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
type ProjectTextSearchResolver = (
|
|
401
|
+
database: ProjectDatabase,
|
|
402
|
+
text: string,
|
|
403
|
+
logger: Logger,
|
|
404
|
+
) => Promise<GlobalSearchHit[]>
|
|
405
|
+
|
|
406
|
+
const projectTextSearchResolvers: ProjectTextSearchResolver[] = [
|
|
407
|
+
async (database, text, logger) => {
|
|
408
|
+
const operations = await database.operation.findMany({
|
|
409
|
+
select: {
|
|
410
|
+
id: true,
|
|
411
|
+
meta: true,
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const matches = operations.filter(
|
|
416
|
+
operation =>
|
|
417
|
+
stringIncludesQuery(operation.id, text) || matchesCommonObjectMeta(operation.meta, text),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return createSearchHits("operation", matches, operation => operation.meta, logger)
|
|
421
|
+
},
|
|
422
|
+
async (database, text, logger) => {
|
|
423
|
+
const states = await database.instanceState.findMany({
|
|
424
|
+
select: {
|
|
425
|
+
id: true,
|
|
426
|
+
instanceId: true,
|
|
427
|
+
status: true,
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const matches = states.filter(
|
|
432
|
+
state =>
|
|
433
|
+
stringIncludesQuery(state.id, text) ||
|
|
434
|
+
stringIncludesQuery(state.instanceId, text) ||
|
|
435
|
+
stringIncludesQuery(`${state.status}`, text),
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return createSearchHits(
|
|
439
|
+
"instanceState",
|
|
440
|
+
matches,
|
|
441
|
+
state => ({
|
|
442
|
+
title: state.instanceId,
|
|
443
|
+
description: `${state.status}`,
|
|
444
|
+
}),
|
|
445
|
+
logger,
|
|
446
|
+
)
|
|
447
|
+
},
|
|
448
|
+
async (database, text, logger) => {
|
|
449
|
+
const artifacts = await database.artifact.findMany({
|
|
450
|
+
select: {
|
|
451
|
+
id: true,
|
|
452
|
+
hash: true,
|
|
453
|
+
meta: true,
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
const matches = artifacts.filter(
|
|
458
|
+
artifact =>
|
|
459
|
+
stringIncludesQuery(artifact.id, text) ||
|
|
460
|
+
stringIncludesQuery(artifact.hash, text) ||
|
|
461
|
+
matchesCommonObjectMeta(artifact.meta, text),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
return createSearchHits("artifact", matches, artifact => artifact.meta, logger)
|
|
465
|
+
},
|
|
466
|
+
async (database, text, logger) => {
|
|
467
|
+
const pages = await database.page.findMany({
|
|
468
|
+
select: {
|
|
469
|
+
id: true,
|
|
470
|
+
name: true,
|
|
471
|
+
meta: true,
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
const matches = pages.filter(
|
|
476
|
+
page =>
|
|
477
|
+
stringIncludesQuery(page.id, text) ||
|
|
478
|
+
stringIncludesQuery(page.name ?? undefined, text) ||
|
|
479
|
+
matchesCommonObjectMeta(page.meta, text),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return createSearchHits("page", matches, page => page.meta, logger)
|
|
483
|
+
},
|
|
484
|
+
async (database, text, logger) => {
|
|
485
|
+
const terminals = await database.terminal.findMany({
|
|
486
|
+
select: {
|
|
487
|
+
id: true,
|
|
488
|
+
name: true,
|
|
489
|
+
meta: true,
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
const matches = terminals.filter(
|
|
494
|
+
terminal =>
|
|
495
|
+
stringIncludesQuery(terminal.id, text) ||
|
|
496
|
+
stringIncludesQuery(terminal.name ?? undefined, text) ||
|
|
497
|
+
matchesCommonObjectMeta(terminal.meta, text),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return createSearchHits("terminal", matches, terminal => terminal.meta, logger)
|
|
501
|
+
},
|
|
502
|
+
async (database, text, logger) => {
|
|
503
|
+
const sessions = await database.terminalSession.findMany({
|
|
504
|
+
select: {
|
|
505
|
+
id: true,
|
|
506
|
+
terminal: {
|
|
507
|
+
select: {
|
|
508
|
+
name: true,
|
|
509
|
+
meta: true,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
const matches = sessions.filter(
|
|
516
|
+
session =>
|
|
517
|
+
stringIncludesQuery(session.id, text) ||
|
|
518
|
+
stringIncludesQuery(session.terminal.name ?? undefined, text) ||
|
|
519
|
+
matchesCommonObjectMeta(session.terminal.meta, text),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
return createSearchHits("terminalSession", matches, session => session.terminal.meta, logger)
|
|
523
|
+
},
|
|
524
|
+
async (database, text, logger) => {
|
|
525
|
+
const secrets = await database.secret.findMany({
|
|
526
|
+
select: {
|
|
527
|
+
id: true,
|
|
528
|
+
name: true,
|
|
529
|
+
systemName: true,
|
|
530
|
+
meta: true,
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const matches = secrets.filter(
|
|
535
|
+
secret =>
|
|
536
|
+
stringIncludesQuery(secret.id, text) ||
|
|
537
|
+
stringIncludesQuery(secret.name ?? undefined, text) ||
|
|
538
|
+
stringIncludesQuery(secret.systemName ?? undefined, text) ||
|
|
539
|
+
matchesCommonObjectMeta(secret.meta, text),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
return createSearchHits("secret", matches, secret => secret.meta, logger)
|
|
543
|
+
},
|
|
544
|
+
async (database, text, logger) => {
|
|
545
|
+
const accounts = await database.serviceAccount.findMany({
|
|
546
|
+
select: {
|
|
547
|
+
id: true,
|
|
548
|
+
meta: true,
|
|
549
|
+
},
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const matches = accounts.filter(
|
|
553
|
+
account =>
|
|
554
|
+
stringIncludesQuery(account.id, text) || matchesCommonObjectMeta(account.meta, text),
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
return createSearchHits("serviceAccount", matches, account => account.meta, logger)
|
|
558
|
+
},
|
|
559
|
+
async (database, text, logger) => {
|
|
560
|
+
const apiKeys = await database.apiKey.findMany({
|
|
561
|
+
select: {
|
|
562
|
+
id: true,
|
|
563
|
+
meta: true,
|
|
564
|
+
},
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
const matches = apiKeys.filter(
|
|
568
|
+
apiKey => stringIncludesQuery(apiKey.id, text) || matchesCommonObjectMeta(apiKey.meta, text),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
return createSearchHits("apiKey", matches, apiKey => apiKey.meta, logger)
|
|
572
|
+
},
|
|
573
|
+
async (database, text, logger) => {
|
|
574
|
+
const triggers = await database.trigger.findMany({
|
|
575
|
+
select: {
|
|
576
|
+
id: true,
|
|
577
|
+
name: true,
|
|
578
|
+
meta: true,
|
|
579
|
+
},
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
const matches = triggers.filter(
|
|
583
|
+
trigger =>
|
|
584
|
+
stringIncludesQuery(trigger.id, text) ||
|
|
585
|
+
stringIncludesQuery(trigger.name, text) ||
|
|
586
|
+
matchesCommonObjectMeta(trigger.meta, text),
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return createSearchHits("trigger", matches, trigger => trigger.meta, logger)
|
|
590
|
+
},
|
|
591
|
+
async (database, text, logger) => {
|
|
592
|
+
const unlockMethods = await database.unlockMethod.findMany({
|
|
593
|
+
select: {
|
|
594
|
+
id: true,
|
|
595
|
+
type: true,
|
|
596
|
+
recipient: true,
|
|
597
|
+
meta: true,
|
|
598
|
+
},
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
const matches = unlockMethods.filter(
|
|
602
|
+
unlockMethod =>
|
|
603
|
+
stringIncludesQuery(unlockMethod.id, text) ||
|
|
604
|
+
stringIncludesQuery(`${unlockMethod.type}`, text) ||
|
|
605
|
+
stringIncludesQuery(unlockMethod.recipient, text) ||
|
|
606
|
+
matchesCommonObjectMeta(unlockMethod.meta, text),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
return createSearchHits("unlockMethod", matches, unlockMethod => unlockMethod.meta, logger)
|
|
610
|
+
},
|
|
611
|
+
async (database, text, logger) => {
|
|
612
|
+
const workers = await database.worker.findMany({
|
|
613
|
+
select: {
|
|
614
|
+
id: true,
|
|
615
|
+
identity: true,
|
|
616
|
+
versions: {
|
|
617
|
+
orderBy: {
|
|
618
|
+
createdAt: "desc",
|
|
619
|
+
},
|
|
620
|
+
take: 1,
|
|
621
|
+
select: {
|
|
622
|
+
meta: true,
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
const matches = workers.filter(
|
|
629
|
+
worker =>
|
|
630
|
+
stringIncludesQuery(worker.id, text) ||
|
|
631
|
+
stringIncludesQuery(worker.identity, text) ||
|
|
632
|
+
matchesCommonObjectMeta(worker.versions[0]?.meta, text),
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
return createSearchHits("worker", matches, worker => worker.versions[0]?.meta, logger)
|
|
636
|
+
},
|
|
637
|
+
async (database, text, logger) => {
|
|
638
|
+
const versions = await database.workerVersion.findMany({
|
|
639
|
+
select: {
|
|
640
|
+
id: true,
|
|
641
|
+
digest: true,
|
|
642
|
+
meta: true,
|
|
643
|
+
},
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
const matches = versions.filter(
|
|
647
|
+
version =>
|
|
648
|
+
stringIncludesQuery(version.id, text) ||
|
|
649
|
+
stringIncludesQuery(version.digest, text) ||
|
|
650
|
+
matchesCommonObjectMeta(version.meta, text),
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
return createSearchHits("workerVersion", matches, version => version.meta, logger)
|
|
654
|
+
},
|
|
655
|
+
async (database, text, logger) => {
|
|
656
|
+
const snapshots = await database.entitySnapshot.findMany({
|
|
657
|
+
select: {
|
|
658
|
+
id: true,
|
|
659
|
+
entityId: true,
|
|
660
|
+
content: {
|
|
661
|
+
select: {
|
|
662
|
+
meta: true,
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
const matches = snapshots.filter(
|
|
669
|
+
snapshot =>
|
|
670
|
+
stringIncludesQuery(snapshot.id, text) ||
|
|
671
|
+
stringIncludesQuery(snapshot.entityId, text) ||
|
|
672
|
+
matchesCommonObjectMeta(snapshot.content.meta, text),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
return createSearchHits("entitySnapshot", matches, snapshot => snapshot.content.meta, logger)
|
|
676
|
+
},
|
|
677
|
+
async (database, text, logger) => {
|
|
678
|
+
const entities = await database.entity.findMany({
|
|
679
|
+
select: {
|
|
680
|
+
id: true,
|
|
681
|
+
type: true,
|
|
682
|
+
identity: true,
|
|
683
|
+
snapshots: {
|
|
684
|
+
orderBy: {
|
|
685
|
+
createdAt: "desc",
|
|
686
|
+
},
|
|
687
|
+
take: 1,
|
|
688
|
+
select: {
|
|
689
|
+
content: {
|
|
690
|
+
select: {
|
|
691
|
+
meta: true,
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
const matches = entities.filter(
|
|
700
|
+
entity =>
|
|
701
|
+
stringIncludesQuery(entity.id, text) ||
|
|
702
|
+
stringIncludesQuery(entity.type, text) ||
|
|
703
|
+
stringIncludesQuery(entity.identity, text) ||
|
|
704
|
+
matchesCommonObjectMeta(entity.snapshots[0]?.content.meta, text),
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
return createSearchHits("entity", matches, entity => entity.snapshots[0]?.content.meta, logger)
|
|
708
|
+
},
|
|
709
|
+
]
|
|
710
|
+
|
|
711
|
+
export class GlobalSearchService {
|
|
712
|
+
constructor(
|
|
713
|
+
private readonly database: DatabaseManager,
|
|
714
|
+
private readonly projectUnlockBackend: ProjectUnlockBackend,
|
|
715
|
+
private readonly logger: Logger,
|
|
716
|
+
) {}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Searches for objects by their IDs across all projects that reference them.
|
|
720
|
+
*
|
|
721
|
+
* For locked projects, it only returns that the project has a match.
|
|
722
|
+
* For unlocked projects, it queries a curated set of collections to find matching objects.
|
|
723
|
+
*
|
|
724
|
+
* @param ids The list of object IDs to search for.
|
|
725
|
+
*/
|
|
726
|
+
async searchByIds(ids: string[]): Promise<GlobalSearchResult[]> {
|
|
727
|
+
const uniqueIds = Array.from(new Set(ids.map(id => id.trim()).filter(Boolean)))
|
|
728
|
+
|
|
729
|
+
if (uniqueIds.length === 0) {
|
|
730
|
+
return []
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const indexed = await this.database.backend.object.findMany({
|
|
734
|
+
where: {
|
|
735
|
+
id: {
|
|
736
|
+
in: uniqueIds,
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
select: {
|
|
740
|
+
id: true,
|
|
741
|
+
projectId: true,
|
|
742
|
+
},
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
const idsByProjectId = new Map<string, Set<string>>()
|
|
746
|
+
|
|
747
|
+
for (const row of indexed) {
|
|
748
|
+
const existingIds = idsByProjectId.get(row.projectId) ?? new Set<string>()
|
|
749
|
+
existingIds.add(row.id)
|
|
750
|
+
idsByProjectId.set(row.projectId, existingIds)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const projectsById = new Map<string, GlobalSearchProjectResult[]>()
|
|
754
|
+
|
|
755
|
+
for (const projectId of idsByProjectId.keys()) {
|
|
756
|
+
const idsInProject = Array.from(idsByProjectId.get(projectId) ?? [])
|
|
757
|
+
|
|
758
|
+
if (idsInProject.length === 0) {
|
|
759
|
+
continue
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const isUnlocked = await this.projectUnlockBackend.checkProjectUnlocked(projectId)
|
|
763
|
+
|
|
764
|
+
if (!isUnlocked) {
|
|
765
|
+
for (const id of idsInProject) {
|
|
766
|
+
const existing = projectsById.get(id) ?? []
|
|
767
|
+
existing.push({ projectId, unlockState: "locked" })
|
|
768
|
+
projectsById.set(id, existing)
|
|
769
|
+
}
|
|
770
|
+
continue
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
try {
|
|
774
|
+
const projectDatabase = await this.database.forProject(projectId)
|
|
775
|
+
const hitsById = await this.searchInUnlockedProject(projectDatabase, idsInProject)
|
|
776
|
+
|
|
777
|
+
for (const id of idsInProject) {
|
|
778
|
+
const hits = hitsById.get(id) ?? []
|
|
779
|
+
|
|
780
|
+
const existing = projectsById.get(id) ?? []
|
|
781
|
+
existing.push({ projectId, unlockState: "unlocked", hits })
|
|
782
|
+
projectsById.set(id, existing)
|
|
783
|
+
}
|
|
784
|
+
} catch (error) {
|
|
785
|
+
this.logger.error(
|
|
786
|
+
{ error, projectId },
|
|
787
|
+
'failed to search in unlocked project "%s"',
|
|
788
|
+
projectId,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
for (const id of idsInProject) {
|
|
792
|
+
const existing = projectsById.get(id) ?? []
|
|
793
|
+
existing.push({ projectId, unlockState: "unlocked", hits: [] })
|
|
794
|
+
projectsById.set(id, existing)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return uniqueIds.map(id => ({ id, projects: projectsById.get(id) ?? [] }))
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Searches for objects across all unlocked projects by a free-form text.
|
|
804
|
+
*
|
|
805
|
+
* It queries a curated set of collections within each unlocked project.
|
|
806
|
+
* Locked projects are skipped because their databases cannot be queried.
|
|
807
|
+
*
|
|
808
|
+
* @param text The text query to search for.
|
|
809
|
+
*/
|
|
810
|
+
async searchByText(text: string): Promise<GlobalSearchTextResult> {
|
|
811
|
+
const normalizedText = normalizeSearchText(text)
|
|
812
|
+
|
|
813
|
+
if (normalizedText.length === 0) {
|
|
814
|
+
return { text, projects: [] }
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const projects = await this.database.backend.project.findMany({
|
|
818
|
+
select: {
|
|
819
|
+
id: true,
|
|
820
|
+
},
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
const results: GlobalSearchTextProjectResult[] = []
|
|
824
|
+
|
|
825
|
+
for (const project of projects) {
|
|
826
|
+
const isUnlocked = await this.projectUnlockBackend.checkProjectUnlocked(project.id)
|
|
827
|
+
|
|
828
|
+
if (!isUnlocked) {
|
|
829
|
+
continue
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
const projectDatabase = await this.database.forProject(project.id)
|
|
834
|
+
const hits = await this.searchInUnlockedProjectByText(projectDatabase, normalizedText)
|
|
835
|
+
|
|
836
|
+
if (hits.length === 0) {
|
|
837
|
+
continue
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
results.push({ projectId: project.id, hits })
|
|
841
|
+
} catch (error) {
|
|
842
|
+
this.logger.error(
|
|
843
|
+
{ error, projectId: project.id },
|
|
844
|
+
'failed to search by text in unlocked project "%s"',
|
|
845
|
+
project.id,
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return { text, projects: results }
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private async searchInUnlockedProject(
|
|
854
|
+
database: ProjectDatabase,
|
|
855
|
+
ids: string[],
|
|
856
|
+
): Promise<Map<string, GlobalSearchHit[]>> {
|
|
857
|
+
const settled = await Promise.allSettled(
|
|
858
|
+
projectSearchResolvers.map(async r => await r(database, ids, this.logger)),
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
const hitsById = new Map<string, GlobalSearchHit[]>()
|
|
862
|
+
|
|
863
|
+
for (const result of settled) {
|
|
864
|
+
if (result.status === "rejected") {
|
|
865
|
+
this.logger.debug({ error: result.reason, ids }, "project search resolver failed")
|
|
866
|
+
continue
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
for (const hit of result.value) {
|
|
870
|
+
const existing = hitsById.get(hit.id) ?? []
|
|
871
|
+
existing.push(hit)
|
|
872
|
+
hitsById.set(hit.id, existing)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return hitsById
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private async searchInUnlockedProjectByText(
|
|
880
|
+
database: ProjectDatabase,
|
|
881
|
+
text: string,
|
|
882
|
+
): Promise<GlobalSearchHit[]> {
|
|
883
|
+
const settled = await Promise.allSettled(
|
|
884
|
+
projectTextSearchResolvers.map(async r => await r(database, text, this.logger)),
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
const hitsByKey = new Map<string, GlobalSearchHit>()
|
|
888
|
+
|
|
889
|
+
for (const result of settled) {
|
|
890
|
+
if (result.status === "rejected") {
|
|
891
|
+
this.logger.debug({ error: result.reason, text }, "project text search resolver failed")
|
|
892
|
+
continue
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
for (const hit of result.value) {
|
|
896
|
+
hitsByKey.set(`${hit.kind}:${hit.id}`, hit)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return Array.from(hitsByKey.values())
|
|
901
|
+
}
|
|
902
|
+
}
|