@kubun/store-graph 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api.d.ts +30 -2
- package/lib/api.js +33 -5
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/migrations.js +1 -1
- package/lib/query-builder.js +18 -9
- package/lib/tables.d.ts +3 -0
- package/package.json +11 -10
package/lib/api.d.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
1
|
import type { Adapter } from '@kubun/db-adapter';
|
|
2
2
|
import { DocumentID, DocumentModelID } from '@kubun/id';
|
|
3
|
-
import type { AggregateDocumentsParams, AggregateValues, DocumentModel, DocumentNode, GroupedAggregateDocumentsParams, GroupedAggregateValue } from '@kubun/protocol';
|
|
3
|
+
import type { AggregateDocumentsParams, AggregateValues, DocumentFieldsMeta, DocumentModel, DocumentModelSchema, DocumentNode, GroupedAggregateDocumentsParams, GroupedAggregateValue } from '@kubun/protocol';
|
|
4
4
|
import type { Kysely } from 'kysely';
|
|
5
|
-
import type { AccessLevel, Catalog, ClusterDefinitionRow, ClusterModel, CreateDocumentParams, CreateGraphParams, GraphModel, GraphModelWithRecord, GraphTables, InsertCatalog, InsertClusterModel, InsertMutationLogEntry, ListDocumentsParams, MutationLogEntry, QueryDocumentsParams, QueryDocumentsResult, SaveDocumentParams, SearchDocumentResult, SearchDocumentsParams } from './tables.js';
|
|
5
|
+
import type { AccessLevel, Catalog, ClusterDefinitionRow, ClusterModel, CreateDocumentParams, CreateGraphParams, DocumentModelRow, GraphModel, GraphModelWithRecord, GraphTables, InsertCatalog, InsertClusterModel, InsertMutationLogEntry, ListDocumentsParams, MutationLogEntry, QueryDocumentsParams, QueryDocumentsResult, SaveDocumentParams, SearchDocumentResult, SearchDocumentsParams } from './tables.js';
|
|
6
|
+
export declare function documentModelFromTable(row: DocumentModelRow): DocumentModel;
|
|
7
|
+
export declare function documentModelToTable(adapter: Adapter, id: string, model: DocumentModel): {
|
|
8
|
+
id: string;
|
|
9
|
+
version: "1.0";
|
|
10
|
+
name: string;
|
|
11
|
+
behavior: "unique" | "default" | "interface";
|
|
12
|
+
unique_fields: string[] | null;
|
|
13
|
+
interfaces: Array<string>;
|
|
14
|
+
schema: DocumentModelSchema;
|
|
15
|
+
fields_meta: DocumentFieldsMeta;
|
|
16
|
+
provider_merge_policy: {
|
|
17
|
+
group: Array<string>;
|
|
18
|
+
} | null;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Reads a document's `field_values` cell on the given transaction and
|
|
22
|
+
* delegates the merge-policy liveness comparison to `@kubun/mutation`'s pure
|
|
23
|
+
* `winnerStateDigestOf`. Returns the winning live `$setFetch` digest, the
|
|
24
|
+
* constant deleted-state digest when the delete side wins, or `null` when the
|
|
25
|
+
* document (or its fetch-order registers) is absent.
|
|
26
|
+
*
|
|
27
|
+
* Because graph and connector tables share one DB connection (the KubunDB
|
|
28
|
+
* coordinator), a caller with its own transaction over a wider `DB` type
|
|
29
|
+
* passes it here cast to `Kysely<GraphTables>` (the same pattern
|
|
30
|
+
* `addDocumentModel` above already relies on) so the read joins that
|
|
31
|
+
* transaction rather than opening a second connection.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readWinnerStateDigest(tx: Kysely<GraphTables>, documentID: string): Promise<string | null>;
|
|
6
34
|
/**
|
|
7
35
|
* Persisted access rule shape as stored in `user_model_access_defaults`.
|
|
8
36
|
*/
|
package/lib/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DocumentID, DocumentModelID } from '@kubun/id';
|
|
2
|
+
import { winnerStateDigestOf } from '@kubun/mutation';
|
|
2
3
|
import { serializeCursor } from './cursor.js';
|
|
3
4
|
import { isMissingTableError, ModelNotDeployedError } from './errors.js';
|
|
4
5
|
import { aggregateFieldName, applyAggregateSelections, applyDocumentFilter, applyDocumentOrderBy, applyGroupBySelections, applyPagination, groupKeyAlias, knownFieldColumn } from './query-builder.js';
|
|
@@ -254,21 +255,22 @@ function compareGroups(a, b, orderBy) {
|
|
|
254
255
|
}
|
|
255
256
|
return 0;
|
|
256
257
|
}
|
|
257
|
-
function documentModelFromTable(row) {
|
|
258
|
+
export function documentModelFromTable(row) {
|
|
258
259
|
const model = {
|
|
259
260
|
version: row.version,
|
|
260
261
|
name: row.name,
|
|
261
262
|
behavior: row.behavior,
|
|
262
263
|
interfaces: row.interfaces,
|
|
263
264
|
schema: row.schema,
|
|
264
|
-
fieldsMeta: row.fields_meta
|
|
265
|
+
fieldsMeta: row.fields_meta,
|
|
266
|
+
providerMergePolicy: row.provider_merge_policy ?? undefined
|
|
265
267
|
};
|
|
266
268
|
if (model.behavior === 'unique') {
|
|
267
269
|
model.uniqueFields = row.unique_fields;
|
|
268
270
|
}
|
|
269
271
|
return model;
|
|
270
272
|
}
|
|
271
|
-
function documentModelToTable(adapter, id, model) {
|
|
273
|
+
export function documentModelToTable(adapter, id, model) {
|
|
272
274
|
return {
|
|
273
275
|
id,
|
|
274
276
|
version: model.version,
|
|
@@ -277,7 +279,8 @@ function documentModelToTable(adapter, id, model) {
|
|
|
277
279
|
unique_fields: model.behavior === 'unique' ? adapter.encodeJSON(model.uniqueFields) : null,
|
|
278
280
|
interfaces: adapter.encodeJSON(model.interfaces),
|
|
279
281
|
schema: adapter.encodeJSON(model.schema),
|
|
280
|
-
fields_meta: adapter.encodeJSON(model.fieldsMeta)
|
|
282
|
+
fields_meta: adapter.encodeJSON(model.fieldsMeta),
|
|
283
|
+
provider_merge_policy: model.providerMergePolicy ? adapter.encodeJSON(model.providerMergePolicy) : null
|
|
281
284
|
};
|
|
282
285
|
}
|
|
283
286
|
function documentNodeFromTable(adapter, doc) {
|
|
@@ -322,6 +325,29 @@ async function addDocumentModel(transaction, adapter, params) {
|
|
|
322
325
|
document_model_id: id
|
|
323
326
|
}).onConflict((oc)=>oc.doNothing()).execute();
|
|
324
327
|
}
|
|
328
|
+
// --- Cross-plan export: tx-scoped merge-policy winner read ---
|
|
329
|
+
/**
|
|
330
|
+
* Reads a document's `field_values` cell on the given transaction and
|
|
331
|
+
* delegates the merge-policy liveness comparison to `@kubun/mutation`'s pure
|
|
332
|
+
* `winnerStateDigestOf`. Returns the winning live `$setFetch` digest, the
|
|
333
|
+
* constant deleted-state digest when the delete side wins, or `null` when the
|
|
334
|
+
* document (or its fetch-order registers) is absent.
|
|
335
|
+
*
|
|
336
|
+
* Because graph and connector tables share one DB connection (the KubunDB
|
|
337
|
+
* coordinator), a caller with its own transaction over a wider `DB` type
|
|
338
|
+
* passes it here cast to `Kysely<GraphTables>` (the same pattern
|
|
339
|
+
* `addDocumentModel` above already relies on) so the read joins that
|
|
340
|
+
* transaction rather than opening a second connection.
|
|
341
|
+
*/ export async function readWinnerStateDigest(tx, documentID) {
|
|
342
|
+
const modelID = DocumentID.fromString(documentID).model.toString();
|
|
343
|
+
const row = await tx.selectFrom(`k_${modelID}`).select('field_values').where('id', '=', documentID).executeTakeFirst();
|
|
344
|
+
if (row?.field_values == null) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
// ParseJSONResultsPlugin may have already parsed the value.
|
|
348
|
+
const fieldValues = typeof row.field_values === 'string' ? JSON.parse(row.field_values) : row.field_values;
|
|
349
|
+
return winnerStateDigestOf(fieldValues);
|
|
350
|
+
}
|
|
325
351
|
// --- Factory function ---
|
|
326
352
|
export function createGraphStore(db, adapter) {
|
|
327
353
|
const coerce = adapter.coerceFilterValue.bind(adapter);
|
|
@@ -443,7 +469,9 @@ export function createGraphStore(db, adapter) {
|
|
|
443
469
|
id,
|
|
444
470
|
owner: params.owner,
|
|
445
471
|
model,
|
|
446
|
-
data
|
|
472
|
+
// Null data (a first-seen tombstone establishing a dead row) persists
|
|
473
|
+
// as SQL null, not the JSON string "null" — mirrors `saveDocument`.
|
|
474
|
+
data: params.data ? adapter.encodeJSON(params.data) : null,
|
|
447
475
|
unique: adapter.encodeBinary(params.unique)
|
|
448
476
|
}).returningAll().executeTakeFirstOrThrow(()=>new Error(`createDocument failed to return row for ${id}`));
|
|
449
477
|
if (params.data) {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { StoreProvider } from '@kubun/db';
|
|
2
2
|
import type { GraphStoreAPI } from './api.js';
|
|
3
3
|
export type { GraphStoreAPI, StoredAccessDefaultRow, StoredAccessRule } from './api.js';
|
|
4
|
-
export { createGraphStore } from './api.js';
|
|
4
|
+
export { createGraphStore, readWinnerStateDigest } from './api.js';
|
|
5
5
|
export { graphStoreDefinition } from './definition.js';
|
|
6
6
|
export { ModelNotDeployedError } from './errors.js';
|
|
7
7
|
export { resolveSearchableFields, SearchNotEnabledError } from './search.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createGraphStore } from './api.js';
|
|
1
|
+
export { createGraphStore, readWinnerStateDigest } from './api.js';
|
|
2
2
|
export { graphStoreDefinition } from './definition.js';
|
|
3
3
|
export { ModelNotDeployedError } from './errors.js';
|
|
4
4
|
export { resolveSearchableFields, SearchNotEnabledError } from './search.js';
|
package/lib/migrations.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
const init = {
|
|
17
17
|
async up (db) {
|
|
18
18
|
// Document models
|
|
19
|
-
await db.schema.createTable('kubun_graph_document_models').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('version', t.text, (col)=>col.notNull()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('behavior', t.text, (col)=>col.notNull()).addColumn('unique_fields', t.json).addColumn('interfaces', t.json, (col)=>col.notNull()).addColumn('schema', t.json, (col)=>col.notNull()).addColumn('fields_meta', t.json, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
|
|
19
|
+
await db.schema.createTable('kubun_graph_document_models').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('version', t.text, (col)=>col.notNull()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('behavior', t.text, (col)=>col.notNull()).addColumn('unique_fields', t.json).addColumn('interfaces', t.json, (col)=>col.notNull()).addColumn('schema', t.json, (col)=>col.notNull()).addColumn('fields_meta', t.json, (col)=>col.notNull()).addColumn('provider_merge_policy', t.json).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
|
|
20
20
|
// Document model interfaces
|
|
21
21
|
await db.schema.createTable('kubun_graph_document_model_interfaces').ifNotExists().addColumn('interface_id', t.text, (col)=>col.notNull().references('kubun_graph_document_models.id')).addColumn('implementation_id', t.text, (col)=>col.notNull().references('kubun_graph_document_models.id')).addUniqueConstraint('kubun_graph_document_model_interfaces_pkey', [
|
|
22
22
|
'interface_id',
|
package/lib/query-builder.js
CHANGED
|
@@ -75,7 +75,9 @@ function getOrderByFieldEntry(field) {
|
|
|
75
75
|
const keys = [];
|
|
76
76
|
let current = field;
|
|
77
77
|
do {
|
|
78
|
-
const
|
|
78
|
+
const entry = Object.entries(current)[0];
|
|
79
|
+
if (entry == null) break;
|
|
80
|
+
const [key, value] = entry;
|
|
79
81
|
keys.push(key);
|
|
80
82
|
if (typeof value === 'string') {
|
|
81
83
|
return {
|
|
@@ -181,11 +183,15 @@ function buildKeysetPredicate(eb, orderBy, values, nullKeys, id, forward, coerce
|
|
|
181
183
|
const idCmp = forward ? '>' : '<';
|
|
182
184
|
const clauses = [];
|
|
183
185
|
for(let level = 0; level < fields.length; level += 1){
|
|
186
|
+
const advanceField = fields[level];
|
|
187
|
+
if (advanceField == null) continue;
|
|
184
188
|
const conjuncts = [];
|
|
185
189
|
for(let prior = 0; prior < level; prior += 1){
|
|
186
|
-
|
|
190
|
+
const priorField = fields[prior];
|
|
191
|
+
if (priorField == null) continue;
|
|
192
|
+
conjuncts.push(equalTerm(eb, priorField));
|
|
187
193
|
}
|
|
188
|
-
conjuncts.push(advanceTerm(eb,
|
|
194
|
+
conjuncts.push(advanceTerm(eb, advanceField));
|
|
189
195
|
clauses.push(eb.and(conjuncts));
|
|
190
196
|
}
|
|
191
197
|
const tieBreak = [];
|
|
@@ -232,10 +238,11 @@ function applyBackwardPagination(queryBuilder, limit, before, orderBy, coerce) {
|
|
|
232
238
|
}
|
|
233
239
|
export function applyDocumentFilter(eb, filter, adapter, path = [], coerce) {
|
|
234
240
|
const entries = Object.entries(filter);
|
|
235
|
-
|
|
241
|
+
const entry = entries[0];
|
|
242
|
+
if (entries.length !== 1 || entry == null) {
|
|
236
243
|
throw new Error('Invalid document filter');
|
|
237
244
|
}
|
|
238
|
-
const [type, value] =
|
|
245
|
+
const [type, value] = entry;
|
|
239
246
|
switch(type){
|
|
240
247
|
case 'where':
|
|
241
248
|
return applyObjectFilter(eb, value, adapter, path, coerce);
|
|
@@ -273,11 +280,12 @@ export function escapeLikePattern(value) {
|
|
|
273
280
|
}
|
|
274
281
|
export function applyValueFilter(eb, keys, filter, adapter, coerce) {
|
|
275
282
|
const entries = Object.entries(filter);
|
|
276
|
-
|
|
283
|
+
const entry = entries[0];
|
|
284
|
+
if (entries.length !== 1 || entry == null) {
|
|
277
285
|
throw new Error('Invalid value filter');
|
|
278
286
|
}
|
|
279
287
|
const fieldName = getKeyPath(eb, keys);
|
|
280
|
-
const [type, value] =
|
|
288
|
+
const [type, value] = entry;
|
|
281
289
|
const c = coerce ?? ((v)=>v);
|
|
282
290
|
switch(type){
|
|
283
291
|
case 'isNull':
|
|
@@ -365,10 +373,11 @@ export function knownFieldColumn(keys) {
|
|
|
365
373
|
}
|
|
366
374
|
function applyOrderByField(query, adapter, orderBy, isReverse, path = []) {
|
|
367
375
|
const entries = Object.entries(orderBy);
|
|
368
|
-
|
|
376
|
+
const entry = entries[0];
|
|
377
|
+
if (entries.length !== 1 || entry == null) {
|
|
369
378
|
throw new Error('Invalid order by field');
|
|
370
379
|
}
|
|
371
|
-
const [key, value] =
|
|
380
|
+
const [key, value] = entry;
|
|
372
381
|
const keys = [
|
|
373
382
|
...path,
|
|
374
383
|
key
|
package/lib/tables.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type DocumentModelTable = {
|
|
|
27
27
|
interfaces: JSONValueColumn<Array<string>>;
|
|
28
28
|
schema: JSONValueColumn<DocumentModelSchema>;
|
|
29
29
|
fields_meta: JSONValueColumn<DocumentFieldsMeta>;
|
|
30
|
+
provider_merge_policy: JSONValueColumn<{
|
|
31
|
+
group: Array<string>;
|
|
32
|
+
}> | null;
|
|
30
33
|
created_at: CreatedAtColumn;
|
|
31
34
|
updated_at: UpdatedAtColumn;
|
|
32
35
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/store-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,16 +15,17 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/db": "^0.
|
|
19
|
-
"@kubun/db-adapter": "^0.
|
|
20
|
-
"@kubun/id": "^0.
|
|
21
|
-
"@kubun/
|
|
18
|
+
"@kubun/db": "^0.17.0",
|
|
19
|
+
"@kubun/db-adapter": "^0.17.0",
|
|
20
|
+
"@kubun/id": "^0.17.0",
|
|
21
|
+
"@kubun/mutation": "^0.17.1",
|
|
22
|
+
"@kubun/protocol": "^0.17.1",
|
|
22
23
|
"@sozai/codec": "^0.4.0",
|
|
23
|
-
"kysely": "^0.29.
|
|
24
|
+
"kysely": "^0.29.6"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
|
-
"@kubun/db-better-sqlite": "^0.
|
|
27
|
-
"@kubun/db-postgres": "^0.
|
|
27
|
+
"@kubun/db-better-sqlite": "^0.17.0",
|
|
28
|
+
"@kubun/db-postgres": "^0.17.1",
|
|
28
29
|
"@testcontainers/postgresql": "^12.1.0"
|
|
29
30
|
},
|
|
30
31
|
"publishConfig": {
|
|
@@ -33,8 +34,8 @@
|
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
35
36
|
"build:clean": "del lib",
|
|
36
|
-
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
37
|
-
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
37
|
+
"build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
38
|
+
"build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
|
|
38
39
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
39
40
|
"test": "pnpm run test:types && pnpm run test:unit",
|
|
40
41
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|