@sixb/sqlite 0.1.7 → 0.1.9
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/action-run-storage.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +0 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +3 -2
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1567 -430
- package/dist/index.js.map +25 -19
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/index.d.ts +3 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/{object-query-compiler.d.ts → objects/query-compiler.d.ts} +16 -1
- package/dist/objects/query-compiler.d.ts.map +1 -0
- package/dist/objects/read-scope.d.ts +17 -0
- package/dist/objects/read-scope.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +51 -0
- package/dist/objects/reader.d.ts.map +1 -0
- package/dist/objects/rows.d.ts +33 -0
- package/dist/objects/rows.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +9 -0
- package/dist/objects/selected-reader.d.ts.map +1 -0
- package/dist/{object-storage.d.ts → objects/storage.d.ts} +12 -9
- package/dist/objects/storage.d.ts.map +1 -0
- package/dist/pipeline-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +20 -0
- package/dist/share-grant-storage.d.ts.map +1 -0
- package/dist/share-session-storage.d.ts +19 -0
- package/dist/share-session-storage.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +1 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/sync-run-storage.d.ts.map +1 -1
- package/dist/transactions.d.ts +8 -0
- package/dist/transactions.d.ts.map +1 -1
- package/dist/workflow-run-storage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/action-run-storage.ts +6 -7
- package/src/agents/rows.ts +0 -2
- package/src/agents/runs.ts +3 -7
- package/src/ai-cost-groups.ts +3 -0
- package/src/ai-cost-storage.ts +11 -2
- package/src/ai-usage-storage.ts +1 -2
- package/src/execution-storage.ts +28 -7
- package/src/index.ts +22 -5
- package/src/migrations/035-share-grants.sql +66 -0
- package/src/migrations/036-share-sessions.sql +226 -0
- package/src/migrations/037-execution-requester-groups.sql +24 -0
- package/src/migrations.ts +9 -0
- package/src/objects/index.ts +2 -0
- package/src/{object-query-compiler.ts → objects/query-compiler.ts} +104 -54
- package/src/objects/read-scope.ts +316 -0
- package/src/objects/reader.ts +526 -0
- package/src/objects/rows.ts +123 -0
- package/src/objects/selected-reader.ts +119 -0
- package/src/objects/storage.ts +296 -0
- package/src/pipeline-run-storage.ts +2 -5
- package/src/share-grant-storage.ts +252 -0
- package/src/share-session-storage.ts +189 -0
- package/src/storage-errors.ts +4 -0
- package/src/sync-run-storage.ts +2 -5
- package/src/transactions.ts +32 -0
- package/src/workflow-run-storage.ts +1 -6
- package/dist/object-query-compiler.d.ts.map +0 -1
- package/dist/object-storage.d.ts.map +0 -1
- package/src/object-storage.ts +0 -709
package/src/object-storage.ts
DELETED
|
@@ -1,709 +0,0 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite"
|
|
2
|
-
import type { ObjectQuery } from "@sixb/core"
|
|
3
|
-
import type {
|
|
4
|
-
CountObjectsInput,
|
|
5
|
-
CountObjectsResult,
|
|
6
|
-
ExistsObjectsInput,
|
|
7
|
-
ExistsObjectsResult,
|
|
8
|
-
ExpandedLinkValue,
|
|
9
|
-
ExpandedObjectRow,
|
|
10
|
-
FacetObjectsInput,
|
|
11
|
-
FacetObjectsResult,
|
|
12
|
-
LinkBatchKey,
|
|
13
|
-
LinkDirection,
|
|
14
|
-
ObjectBatchKey,
|
|
15
|
-
ObjectFacetRequest,
|
|
16
|
-
ObjectLinkRow,
|
|
17
|
-
ObjectQueryCapabilities,
|
|
18
|
-
ObjectRow,
|
|
19
|
-
ObjectRowLinks,
|
|
20
|
-
ObjectStorage,
|
|
21
|
-
QueryObjectLinksInput,
|
|
22
|
-
QueryObjectLinksResult,
|
|
23
|
-
QueryObjectsInput,
|
|
24
|
-
QueryObjectsResult,
|
|
25
|
-
} from "@sixb/core/storage"
|
|
26
|
-
import { linkBatchKey, objectBatchKey } from "@sixb/core/storage"
|
|
27
|
-
import { installFreshSqliteSchema } from "./migrations"
|
|
28
|
-
import { type CompiledObjectQuery, compileObjectQuery } from "./object-query-compiler"
|
|
29
|
-
import {
|
|
30
|
-
closeSqliteStoreConnection,
|
|
31
|
-
openSqliteStoreConnection,
|
|
32
|
-
type SqliteStoreConnection,
|
|
33
|
-
} from "./transactions"
|
|
34
|
-
|
|
35
|
-
export interface SqliteObjectStorageOptions {
|
|
36
|
-
/** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
|
|
37
|
-
path?: string
|
|
38
|
-
/** Internal shared connection used by bundled SqliteStorage. */
|
|
39
|
-
connection?: SqliteStoreConnection
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
43
|
-
queryObjects: true,
|
|
44
|
-
countObjects: true,
|
|
45
|
-
existsObjects: true,
|
|
46
|
-
facetObjects: true,
|
|
47
|
-
nodes: {
|
|
48
|
-
start: true,
|
|
49
|
-
refs: true,
|
|
50
|
-
filter: true,
|
|
51
|
-
text: true,
|
|
52
|
-
sort: true,
|
|
53
|
-
limit: true,
|
|
54
|
-
page: true,
|
|
55
|
-
traverse: true,
|
|
56
|
-
set: true,
|
|
57
|
-
project: true,
|
|
58
|
-
expand: true,
|
|
59
|
-
},
|
|
60
|
-
predicateOps: {
|
|
61
|
-
and: true,
|
|
62
|
-
or: true,
|
|
63
|
-
not: true,
|
|
64
|
-
eq: true,
|
|
65
|
-
neq: true,
|
|
66
|
-
lt: true,
|
|
67
|
-
lte: true,
|
|
68
|
-
gt: true,
|
|
69
|
-
gte: true,
|
|
70
|
-
in: true,
|
|
71
|
-
exists: true,
|
|
72
|
-
contains: true,
|
|
73
|
-
},
|
|
74
|
-
sortKinds: {
|
|
75
|
-
property: true,
|
|
76
|
-
},
|
|
77
|
-
traversalDirections: {
|
|
78
|
-
outgoing: true,
|
|
79
|
-
incoming: true,
|
|
80
|
-
},
|
|
81
|
-
setOps: {
|
|
82
|
-
union: true,
|
|
83
|
-
intersect: true,
|
|
84
|
-
subtract: true,
|
|
85
|
-
},
|
|
86
|
-
scalarOperations: {
|
|
87
|
-
string: { equality: true, ordering: true },
|
|
88
|
-
uuid: { equality: true, ordering: true },
|
|
89
|
-
boolean: { equality: true },
|
|
90
|
-
integer: { equality: true, ordering: true },
|
|
91
|
-
double: { equality: true, ordering: true },
|
|
92
|
-
decimal: { equality: true },
|
|
93
|
-
date: { equality: true, ordering: true },
|
|
94
|
-
timestamp: { equality: true, ordering: true },
|
|
95
|
-
},
|
|
96
|
-
limits: {
|
|
97
|
-
totalCount: true,
|
|
98
|
-
stablePageTokens: true,
|
|
99
|
-
},
|
|
100
|
-
notes: [
|
|
101
|
-
"SQLite object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
|
|
102
|
-
"expand hydrates linked objects in-database (top-N per parent via row_number() + json_group_array); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
|
|
103
|
-
"Ordered decimal predicates and sorting use the bounded core fallback because SQLite has no native exact decimal type; canonical decimal equality remains pushdown-safe.",
|
|
104
|
-
"Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
|
|
105
|
-
],
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* SQLite-based ObjectStorage implementation.
|
|
110
|
-
*
|
|
111
|
-
* Stores object projections and links. V1 object-query IR pushdown covers the
|
|
112
|
-
* scalar JSON-property and link-traversal subset declared by queryCapabilities().
|
|
113
|
-
*/
|
|
114
|
-
export class SqliteObjectStorage implements ObjectStorage {
|
|
115
|
-
private readonly connection: SqliteStoreConnection
|
|
116
|
-
private readonly db: Database
|
|
117
|
-
|
|
118
|
-
constructor(options: SqliteObjectStorageOptions = {}) {
|
|
119
|
-
this.connection = openSqliteStoreConnection(options)
|
|
120
|
-
this.db = this.connection.db
|
|
121
|
-
|
|
122
|
-
if (this.connection.installFreshSchema) {
|
|
123
|
-
installFreshSqliteSchema(this.db)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
queryCapabilities(): ObjectQueryCapabilities {
|
|
128
|
-
return SQLITE_OBJECT_QUERY_CAPABILITIES
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
|
|
132
|
-
const compiled = compileObjectQuery(params.projectId, params.query, {
|
|
133
|
-
includeTotal: params.includeTotal,
|
|
134
|
-
})
|
|
135
|
-
const total = params.includeTotal === false ? undefined : readTotal(this.db, compiled)
|
|
136
|
-
const rawRows = this.db.query(compiled.sql).all(...compiled.args) as ObjectQueryDatabaseRow[]
|
|
137
|
-
const rows = compiled.trimRows(rawRows) as readonly ObjectQueryDatabaseRow[]
|
|
138
|
-
const hasMore =
|
|
139
|
-
total === undefined && compiled.hasMoreProbe
|
|
140
|
-
? compiled.hasMoreProbe.hasMore(
|
|
141
|
-
this.db.query(compiled.hasMoreProbe.sql).all(...compiled.hasMoreProbe.args).length
|
|
142
|
-
)
|
|
143
|
-
: compiled.hasMore(rawRows.length, total)
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
objects: rows.map((row) => this.queryRowToObject(row)),
|
|
147
|
-
hasMore,
|
|
148
|
-
nextPageToken: compiled.nextPageToken(rows, rawRows.length),
|
|
149
|
-
...(total === undefined ? {} : { total }),
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
|
|
154
|
-
return {
|
|
155
|
-
count: readTotal(
|
|
156
|
-
this.db,
|
|
157
|
-
compileObjectQuery(params.projectId, stripOuterRowShape(params.query))
|
|
158
|
-
),
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
|
|
163
|
-
const compiled = compileObjectQuery(params.projectId, existsProbeQuery(params.query))
|
|
164
|
-
return { exists: this.db.query(compiled.sql).get(...compiled.args) !== null }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
|
|
168
|
-
const compiled = compileObjectQuery(params.projectId, stripOuterRowShape(params.query))
|
|
169
|
-
return {
|
|
170
|
-
facets: params.facets.map((facet) => ({
|
|
171
|
-
propertyId: facet.propertyId,
|
|
172
|
-
buckets: readFacetBuckets(this.db, compiled, facet),
|
|
173
|
-
})),
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async getByPrimaryId(params: {
|
|
178
|
-
projectId: string
|
|
179
|
-
objectTypeId: string
|
|
180
|
-
primaryId: string
|
|
181
|
-
}): Promise<ObjectRow | null> {
|
|
182
|
-
const row = this.db
|
|
183
|
-
.query("SELECT * FROM objects WHERE project_id = ? AND object_type_id = ? AND primary_id = ?")
|
|
184
|
-
.get(params.projectId, params.objectTypeId, params.primaryId) as DatabaseRow | null
|
|
185
|
-
|
|
186
|
-
return row ? this.rowToObject(row) : null
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async listLinks(params: {
|
|
190
|
-
projectId: string
|
|
191
|
-
objectTypeId: string
|
|
192
|
-
objectId: string
|
|
193
|
-
linkId?: string
|
|
194
|
-
direction?: LinkDirection
|
|
195
|
-
}): Promise<readonly ObjectLinkRow[]> {
|
|
196
|
-
const direction = params.direction ?? "outgoing"
|
|
197
|
-
const directionWhere =
|
|
198
|
-
direction === "incoming"
|
|
199
|
-
? "target_type_id = ? AND target_id = ?"
|
|
200
|
-
: direction === "both"
|
|
201
|
-
? "((source_type_id = ? AND source_id = ?) OR (target_type_id = ? AND target_id = ?))"
|
|
202
|
-
: "source_type_id = ? AND source_id = ?"
|
|
203
|
-
let query = `SELECT * FROM links WHERE project_id = ? AND ${directionWhere}`
|
|
204
|
-
const args: (string | number)[] =
|
|
205
|
-
direction === "both"
|
|
206
|
-
? [
|
|
207
|
-
params.projectId,
|
|
208
|
-
params.objectTypeId,
|
|
209
|
-
params.objectId,
|
|
210
|
-
params.objectTypeId,
|
|
211
|
-
params.objectId,
|
|
212
|
-
]
|
|
213
|
-
: [params.projectId, params.objectTypeId, params.objectId]
|
|
214
|
-
|
|
215
|
-
if (params.linkId) {
|
|
216
|
-
query += " AND link_id = ?"
|
|
217
|
-
args.push(params.linkId)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const rows = this.db.query(query).all(...args) as LinkDatabaseRow[]
|
|
221
|
-
|
|
222
|
-
return rows.map((row) => this.rowToLink(row))
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async getByPrimaryIdBatch(params: {
|
|
226
|
-
projectId: string
|
|
227
|
-
items: readonly { objectTypeId: string; primaryId: string }[]
|
|
228
|
-
}): Promise<Map<ObjectBatchKey, ObjectRow>> {
|
|
229
|
-
const result = new Map<ObjectBatchKey, ObjectRow>()
|
|
230
|
-
if (params.items.length === 0) return result
|
|
231
|
-
|
|
232
|
-
const rows = this.db
|
|
233
|
-
.query(
|
|
234
|
-
`
|
|
235
|
-
SELECT object.*
|
|
236
|
-
FROM json_each(?) AS requested
|
|
237
|
-
JOIN objects AS object
|
|
238
|
-
ON object.project_id = ?
|
|
239
|
-
AND object.object_type_id = json_extract(requested.value, '$.objectTypeId')
|
|
240
|
-
AND object.primary_id = json_extract(requested.value, '$.primaryId')
|
|
241
|
-
`
|
|
242
|
-
)
|
|
243
|
-
.all(JSON.stringify(params.items), params.projectId) as DatabaseRow[]
|
|
244
|
-
for (const row of rows) {
|
|
245
|
-
result.set(objectBatchKey(row.object_type_id, row.primary_id), this.rowToObject(row))
|
|
246
|
-
}
|
|
247
|
-
return result
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async listLinksBatch(params: {
|
|
251
|
-
projectId: string
|
|
252
|
-
items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
|
|
253
|
-
}): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
|
|
254
|
-
const result = new Map<LinkBatchKey, ObjectLinkRow[]>()
|
|
255
|
-
if (params.items.length === 0) return result
|
|
256
|
-
|
|
257
|
-
const stmt = this.db.query(
|
|
258
|
-
"SELECT * FROM links WHERE project_id = ? AND source_type_id = ? AND source_id = ? AND link_id = ?"
|
|
259
|
-
)
|
|
260
|
-
for (const item of params.items) {
|
|
261
|
-
const rows = stmt.all(
|
|
262
|
-
params.projectId,
|
|
263
|
-
item.objectTypeId,
|
|
264
|
-
item.objectId,
|
|
265
|
-
item.linkId
|
|
266
|
-
) as LinkDatabaseRow[]
|
|
267
|
-
if (rows.length > 0) {
|
|
268
|
-
result.set(
|
|
269
|
-
linkBatchKey(item.objectTypeId, item.objectId, item.linkId),
|
|
270
|
-
rows.map((r) => this.rowToLink(r))
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return result
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
|
|
278
|
-
assertLinkQueryLimit(params.limit)
|
|
279
|
-
if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
|
|
280
|
-
return { links: [], hasMore: false }
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const sourceJoin = `
|
|
284
|
-
SELECT link.*
|
|
285
|
-
FROM links AS link
|
|
286
|
-
JOIN requested
|
|
287
|
-
ON requested.object_type_id = link.source_type_id
|
|
288
|
-
AND requested.object_id = link.source_id
|
|
289
|
-
WHERE link.project_id = ?
|
|
290
|
-
`
|
|
291
|
-
const targetJoin = `
|
|
292
|
-
SELECT link.*
|
|
293
|
-
FROM links AS link
|
|
294
|
-
JOIN requested
|
|
295
|
-
ON requested.object_type_id = link.target_type_id
|
|
296
|
-
AND requested.object_id = link.target_id
|
|
297
|
-
WHERE link.project_id = ?
|
|
298
|
-
`
|
|
299
|
-
const incidentSql =
|
|
300
|
-
params.direction === "outgoing"
|
|
301
|
-
? sourceJoin
|
|
302
|
-
: params.direction === "incoming"
|
|
303
|
-
? targetJoin
|
|
304
|
-
: `${sourceJoin} UNION ${targetJoin}`
|
|
305
|
-
const args: (string | number)[] = [JSON.stringify(params.objectRefs), params.projectId]
|
|
306
|
-
if (params.direction === "both") args.push(params.projectId)
|
|
307
|
-
|
|
308
|
-
const predicates: string[] = []
|
|
309
|
-
if (params.linkId !== undefined) {
|
|
310
|
-
predicates.push("link_id = ?")
|
|
311
|
-
args.push(params.linkId)
|
|
312
|
-
}
|
|
313
|
-
if (params.endpointObjectTypeIds !== undefined) {
|
|
314
|
-
const allowedTypes = JSON.stringify([...new Set(params.endpointObjectTypeIds)])
|
|
315
|
-
predicates.push(
|
|
316
|
-
"source_type_id IN (SELECT value FROM json_each(?))",
|
|
317
|
-
"target_type_id IN (SELECT value FROM json_each(?))"
|
|
318
|
-
)
|
|
319
|
-
args.push(allowedTypes, allowedTypes)
|
|
320
|
-
}
|
|
321
|
-
if (params.after) {
|
|
322
|
-
predicates.push(
|
|
323
|
-
"(source_type_id, source_id, link_id, target_type_id, target_id) > (?, ?, ?, ?, ?)"
|
|
324
|
-
)
|
|
325
|
-
args.push(...params.after)
|
|
326
|
-
}
|
|
327
|
-
args.push(params.limit + 1)
|
|
328
|
-
|
|
329
|
-
const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : ""
|
|
330
|
-
const rows = this.db
|
|
331
|
-
.query(
|
|
332
|
-
`
|
|
333
|
-
WITH requested AS (
|
|
334
|
-
SELECT DISTINCT
|
|
335
|
-
json_extract(value, '$.objectTypeId') AS object_type_id,
|
|
336
|
-
json_extract(value, '$.primaryId') AS object_id
|
|
337
|
-
FROM json_each(?)
|
|
338
|
-
), incident AS (${incidentSql})
|
|
339
|
-
SELECT *
|
|
340
|
-
FROM incident
|
|
341
|
-
${whereSql}
|
|
342
|
-
ORDER BY source_type_id, source_id, link_id, target_type_id, target_id
|
|
343
|
-
LIMIT ?
|
|
344
|
-
`
|
|
345
|
-
)
|
|
346
|
-
.all(...args) as LinkDatabaseRow[]
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
links: rows.slice(0, params.limit).map((row) => this.rowToLink(row)),
|
|
350
|
-
hasMore: rows.length > params.limit,
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
async listIncidentLinksBatch(params: {
|
|
355
|
-
projectId: string
|
|
356
|
-
items: readonly { objectTypeId: string; objectId: string }[]
|
|
357
|
-
}): Promise<readonly ObjectLinkRow[]> {
|
|
358
|
-
if (params.items.length === 0) return []
|
|
359
|
-
|
|
360
|
-
const rows = this.db
|
|
361
|
-
.query(
|
|
362
|
-
`
|
|
363
|
-
WITH requested AS (
|
|
364
|
-
SELECT
|
|
365
|
-
json_extract(value, '$.objectTypeId') AS object_type_id,
|
|
366
|
-
json_extract(value, '$.objectId') AS object_id
|
|
367
|
-
FROM json_each(?)
|
|
368
|
-
)
|
|
369
|
-
SELECT link.*
|
|
370
|
-
FROM links AS link
|
|
371
|
-
JOIN requested
|
|
372
|
-
ON requested.object_type_id = link.source_type_id
|
|
373
|
-
AND requested.object_id = link.source_id
|
|
374
|
-
WHERE link.project_id = ?
|
|
375
|
-
UNION
|
|
376
|
-
SELECT link.*
|
|
377
|
-
FROM links AS link
|
|
378
|
-
JOIN requested
|
|
379
|
-
ON requested.object_type_id = link.target_type_id
|
|
380
|
-
AND requested.object_id = link.target_id
|
|
381
|
-
WHERE link.project_id = ?
|
|
382
|
-
`
|
|
383
|
-
)
|
|
384
|
-
.all(JSON.stringify(params.items), params.projectId, params.projectId) as LinkDatabaseRow[]
|
|
385
|
-
|
|
386
|
-
return rows.map((row) => this.rowToLink(row))
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async listByPrimaryIdPage(params: {
|
|
390
|
-
projectId: string
|
|
391
|
-
objectTypeId: string
|
|
392
|
-
afterPrimaryId?: string
|
|
393
|
-
limit: number
|
|
394
|
-
}): Promise<{ objects: readonly ObjectRow[]; nextPrimaryId?: string }> {
|
|
395
|
-
assertReconciliationPageLimit(params.limit)
|
|
396
|
-
const rows = this.db
|
|
397
|
-
.query(
|
|
398
|
-
`
|
|
399
|
-
SELECT * FROM objects
|
|
400
|
-
WHERE project_id = ? AND object_type_id = ?
|
|
401
|
-
AND (? IS NULL OR primary_id > ?)
|
|
402
|
-
ORDER BY primary_id ASC
|
|
403
|
-
LIMIT ?
|
|
404
|
-
`
|
|
405
|
-
)
|
|
406
|
-
.all(
|
|
407
|
-
params.projectId,
|
|
408
|
-
params.objectTypeId,
|
|
409
|
-
params.afterPrimaryId ?? null,
|
|
410
|
-
params.afterPrimaryId ?? null,
|
|
411
|
-
params.limit + 1
|
|
412
|
-
) as DatabaseRow[]
|
|
413
|
-
const hasMore = rows.length > params.limit
|
|
414
|
-
const objects = rows.slice(0, params.limit).map((row) => this.rowToObject(row))
|
|
415
|
-
const last = objects.at(-1)
|
|
416
|
-
return {
|
|
417
|
-
objects,
|
|
418
|
-
...(hasMore && last ? { nextPrimaryId: last.primaryId } : {}),
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async list(params: {
|
|
423
|
-
projectId: string
|
|
424
|
-
objectTypeId?: string | readonly string[]
|
|
425
|
-
primaryIdPrefix?: string
|
|
426
|
-
primaryIdSuffix?: string
|
|
427
|
-
updatedAfter?: Date
|
|
428
|
-
updatedBefore?: Date
|
|
429
|
-
createdAfter?: Date
|
|
430
|
-
createdBefore?: Date
|
|
431
|
-
limit?: number
|
|
432
|
-
offset?: number
|
|
433
|
-
orderBy?: "createdAt" | "updatedAt" | "primaryId"
|
|
434
|
-
order?: "asc" | "desc"
|
|
435
|
-
}): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
|
|
436
|
-
let query = "SELECT * FROM objects WHERE project_id = ?"
|
|
437
|
-
const args: (string | number | null)[] = [params.projectId]
|
|
438
|
-
|
|
439
|
-
if (params.objectTypeId) {
|
|
440
|
-
if (typeof params.objectTypeId === "string") {
|
|
441
|
-
query += " AND object_type_id = ?"
|
|
442
|
-
args.push(params.objectTypeId)
|
|
443
|
-
} else {
|
|
444
|
-
query += ` AND object_type_id IN (${params.objectTypeId.map(() => "?").join(", ")})`
|
|
445
|
-
args.push(...params.objectTypeId)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (params.primaryIdPrefix) {
|
|
450
|
-
query += " AND primary_id LIKE ?"
|
|
451
|
-
args.push(`${params.primaryIdPrefix}%`)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (params.primaryIdSuffix) {
|
|
455
|
-
query += " AND primary_id LIKE ?"
|
|
456
|
-
args.push(`%${params.primaryIdSuffix}`)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (params.updatedAfter) {
|
|
460
|
-
query += " AND updated_at >= ?"
|
|
461
|
-
args.push(params.updatedAfter.toISOString())
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (params.updatedBefore) {
|
|
465
|
-
query += " AND updated_at <= ?"
|
|
466
|
-
args.push(params.updatedBefore.toISOString())
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (params.createdAfter) {
|
|
470
|
-
query += " AND created_at >= ?"
|
|
471
|
-
args.push(params.createdAfter.toISOString())
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (params.createdBefore) {
|
|
475
|
-
query += " AND created_at <= ?"
|
|
476
|
-
args.push(params.createdBefore.toISOString())
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Get total count
|
|
480
|
-
const countResult = this.db.query(`SELECT COUNT(*) as total FROM (${query})`).get(...args) as {
|
|
481
|
-
total: number
|
|
482
|
-
}
|
|
483
|
-
const total = countResult.total
|
|
484
|
-
|
|
485
|
-
const offset = params.offset ?? 0
|
|
486
|
-
const limit = params.limit ?? 50
|
|
487
|
-
if (limit === 0) return { objects: [], hasMore: offset < total, total }
|
|
488
|
-
|
|
489
|
-
// Add ordering
|
|
490
|
-
const orderBy = params.orderBy ?? "updatedAt"
|
|
491
|
-
const order = params.order ?? "desc"
|
|
492
|
-
const orderColumn =
|
|
493
|
-
orderBy === "primaryId" ? "primary_id" : orderBy === "createdAt" ? "created_at" : "updated_at"
|
|
494
|
-
query += ` ORDER BY ${orderColumn} ${order.toUpperCase()}`
|
|
495
|
-
|
|
496
|
-
// Add pagination
|
|
497
|
-
query += " LIMIT ? OFFSET ?"
|
|
498
|
-
args.push(limit + 1, offset) // +1 to check for hasMore
|
|
499
|
-
|
|
500
|
-
const rows = this.db.query(query).all(...args) as DatabaseRow[]
|
|
501
|
-
const hasMore = rows.length > limit
|
|
502
|
-
const objects = rows.slice(0, limit).map((row) => this.rowToObject(row))
|
|
503
|
-
|
|
504
|
-
return { objects, hasMore, total }
|
|
505
|
-
}
|
|
506
|
-
private rowToObject(row: DatabaseRow): ObjectRow {
|
|
507
|
-
return {
|
|
508
|
-
projectId: row.project_id,
|
|
509
|
-
objectTypeId: row.object_type_id,
|
|
510
|
-
primaryId: row.primary_id,
|
|
511
|
-
properties: JSON.parse(row.properties),
|
|
512
|
-
createdAt: new Date(row.created_at),
|
|
513
|
-
updatedAt: new Date(row.updated_at),
|
|
514
|
-
version: row.version,
|
|
515
|
-
lastCommitId: row.last_commit_id,
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Map a query row, attaching `links` when an `expand` pushdown produced them.
|
|
520
|
-
// The base columns map as usual; `_expand` (a `json_object` serialized to TEXT)
|
|
521
|
-
// is parsed and revived into the runtime link shape the core executor's
|
|
522
|
-
// fallback also produces.
|
|
523
|
-
private queryRowToObject(row: ObjectQueryDatabaseRow): ObjectRow {
|
|
524
|
-
const base = this.rowToObject(row)
|
|
525
|
-
const links = reviveExpandedLinks(parseExpandColumn(row._expand))
|
|
526
|
-
return links ? { ...base, links } : base
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
private rowToLink(row: LinkDatabaseRow): ObjectLinkRow {
|
|
530
|
-
return {
|
|
531
|
-
projectId: row.project_id,
|
|
532
|
-
sourceTypeId: row.source_type_id,
|
|
533
|
-
sourceId: row.source_id,
|
|
534
|
-
linkId: row.link_id,
|
|
535
|
-
targetTypeId: row.target_type_id,
|
|
536
|
-
targetId: row.target_id,
|
|
537
|
-
properties: row.properties ? JSON.parse(row.properties) : undefined,
|
|
538
|
-
createdAt: new Date(row.created_at),
|
|
539
|
-
updatedAt: new Date(row.updated_at),
|
|
540
|
-
lastCommitId: row.last_commit_id,
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Close the database connection.
|
|
546
|
-
*/
|
|
547
|
-
close(): void {
|
|
548
|
-
closeSqliteStoreConnection(this.connection)
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function assertReconciliationPageLimit(limit: number): void {
|
|
553
|
-
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
554
|
-
throw new Error("Object reconciliation page limit must be a positive safe integer.")
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function assertLinkQueryLimit(limit: number): void {
|
|
559
|
-
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
560
|
-
throw new Error("Object link query limit must be a positive safe integer.")
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function readTotal(db: Database, compiled: CompiledObjectQuery): number {
|
|
565
|
-
const row = db.query(compiled.totalSql).get(...compiled.totalArgs) as { total: number }
|
|
566
|
-
return row.total
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function readFacetBuckets(
|
|
570
|
-
db: Database,
|
|
571
|
-
compiled: CompiledObjectQuery,
|
|
572
|
-
facet: ObjectFacetRequest
|
|
573
|
-
): { value: unknown; count: number }[] {
|
|
574
|
-
const path = sqliteJsonPath(facet.propertyId)
|
|
575
|
-
const rows = db
|
|
576
|
-
.query(
|
|
577
|
-
`
|
|
578
|
-
SELECT
|
|
579
|
-
json_type(input.properties, ?) AS value_type,
|
|
580
|
-
json_extract(input.properties, ?) AS value,
|
|
581
|
-
COUNT(*) AS count
|
|
582
|
-
FROM (${compiled.sql}) AS input
|
|
583
|
-
WHERE json_type(input.properties, ?) IS NOT NULL
|
|
584
|
-
GROUP BY value_type, value
|
|
585
|
-
ORDER BY count DESC, CAST(value AS TEXT) ASC
|
|
586
|
-
LIMIT ?
|
|
587
|
-
`
|
|
588
|
-
)
|
|
589
|
-
.all(path, path, ...compiled.args, path, facet.limit) as FacetDatabaseRow[]
|
|
590
|
-
|
|
591
|
-
return rows.map((row) => ({
|
|
592
|
-
value: sqliteFacetValue(row.value_type, row.value),
|
|
593
|
-
count: Number(row.count),
|
|
594
|
-
}))
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function sqliteJsonPath(propertyId: string): string {
|
|
598
|
-
return `$."${propertyId.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function sqliteFacetValue(valueType: string | null, value: unknown): unknown {
|
|
602
|
-
if (valueType === "true") return true
|
|
603
|
-
if (valueType === "false") return false
|
|
604
|
-
if (valueType === "null") return null
|
|
605
|
-
return value
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// The `_expand` column comes back from SQLite as a JSON string (or null/absent
|
|
609
|
-
// when the query carried no expansion); parse it before reviving.
|
|
610
|
-
function parseExpandColumn(value: unknown): unknown {
|
|
611
|
-
if (typeof value !== "string") return value ?? undefined
|
|
612
|
-
return JSON.parse(value)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function reviveExpandedLinks(value: unknown): ObjectRowLinks | undefined {
|
|
616
|
-
if (!isPlainObject(value)) return undefined
|
|
617
|
-
const links: ObjectRowLinks = {}
|
|
618
|
-
for (const [linkId, raw] of Object.entries(value)) {
|
|
619
|
-
links[linkId] = reviveExpandedLinkValue(raw)
|
|
620
|
-
}
|
|
621
|
-
return links
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// A "one" expansion arrives as a single object or null; a "many" expansion as an
|
|
625
|
-
// array (already ordered and trimmed in the database).
|
|
626
|
-
function reviveExpandedLinkValue(value: unknown): ExpandedLinkValue {
|
|
627
|
-
if (value === null || value === undefined) return null
|
|
628
|
-
if (Array.isArray(value)) return value.map(reviveExpandedRow)
|
|
629
|
-
return reviveExpandedRow(value)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Revive one hydrated neighbour from `compileExpansionChildJson`: timestamp
|
|
633
|
-
// strings back to `Date`, null/empty link properties dropped (matching the
|
|
634
|
-
// fallback), and nested links recursed.
|
|
635
|
-
function reviveExpandedRow(value: unknown): ExpandedObjectRow {
|
|
636
|
-
const row = isPlainObject(value) ? value : {}
|
|
637
|
-
const expanded: ExpandedObjectRow = {
|
|
638
|
-
projectId: String(row.projectId),
|
|
639
|
-
objectTypeId: String(row.objectTypeId),
|
|
640
|
-
primaryId: String(row.primaryId),
|
|
641
|
-
properties: isPlainObject(row.properties) ? row.properties : {},
|
|
642
|
-
createdAt: new Date(row.createdAt as string),
|
|
643
|
-
updatedAt: new Date(row.updatedAt as string),
|
|
644
|
-
version: Number(row.version),
|
|
645
|
-
lastCommitId: String(row.lastCommitId),
|
|
646
|
-
}
|
|
647
|
-
if (isPlainObject(row.linkProperties) && Object.keys(row.linkProperties).length > 0) {
|
|
648
|
-
expanded.linkProperties = row.linkProperties
|
|
649
|
-
}
|
|
650
|
-
const links = reviveExpandedLinks(row.links)
|
|
651
|
-
if (links) expanded.links = links
|
|
652
|
-
return expanded
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
656
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function existsProbeQuery(query: ObjectQuery): ObjectQuery {
|
|
660
|
-
return { kind: "limit", limit: 1, input: stripOuterRowShape(query) }
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function stripOuterRowShape(query: ObjectQuery): ObjectQuery {
|
|
664
|
-
switch (query.kind) {
|
|
665
|
-
case "limit":
|
|
666
|
-
case "page":
|
|
667
|
-
case "project":
|
|
668
|
-
case "sort":
|
|
669
|
-
return stripOuterRowShape(query.input)
|
|
670
|
-
default:
|
|
671
|
-
return query
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
interface DatabaseRow {
|
|
676
|
-
project_id: string
|
|
677
|
-
object_type_id: string
|
|
678
|
-
primary_id: string
|
|
679
|
-
properties: string
|
|
680
|
-
created_at: string
|
|
681
|
-
updated_at: string
|
|
682
|
-
version: number
|
|
683
|
-
last_commit_id: string
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
interface ObjectQueryDatabaseRow extends DatabaseRow {
|
|
687
|
-
_cursor_properties?: string
|
|
688
|
-
/** `json_object(linkId, value, ...)` serialized text from an `expand` pushdown; absent otherwise. */
|
|
689
|
-
_expand?: string | null
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
interface FacetDatabaseRow {
|
|
693
|
-
value_type: string | null
|
|
694
|
-
value: unknown
|
|
695
|
-
count: number
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
interface LinkDatabaseRow {
|
|
699
|
-
project_id: string
|
|
700
|
-
source_type_id: string
|
|
701
|
-
source_id: string
|
|
702
|
-
link_id: string
|
|
703
|
-
target_type_id: string
|
|
704
|
-
target_id: string
|
|
705
|
-
properties: string | null
|
|
706
|
-
created_at: string
|
|
707
|
-
updated_at: string
|
|
708
|
-
last_commit_id: string
|
|
709
|
-
}
|