@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 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: adapter.encodeJSON(params.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',
@@ -75,7 +75,9 @@ function getOrderByFieldEntry(field) {
75
75
  const keys = [];
76
76
  let current = field;
77
77
  do {
78
- const [key, value] = Object.entries(current)[0];
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
- conjuncts.push(equalTerm(eb, fields[prior]));
190
+ const priorField = fields[prior];
191
+ if (priorField == null) continue;
192
+ conjuncts.push(equalTerm(eb, priorField));
187
193
  }
188
- conjuncts.push(advanceTerm(eb, fields[level]));
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
- if (entries.length !== 1) {
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] = entries[0];
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
- if (entries.length !== 1) {
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] = entries[0];
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
- if (entries.length !== 1) {
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] = entries[0];
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.16.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.16.0",
19
- "@kubun/db-adapter": "^0.16.0",
20
- "@kubun/id": "^0.16.0",
21
- "@kubun/protocol": "^0.16.0",
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.5"
24
+ "kysely": "^0.29.6"
24
25
  },
25
26
  "devDependencies": {
26
- "@kubun/db-better-sqlite": "^0.16.0",
27
- "@kubun/db-postgres": "^0.16.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",