@kubun/store-graph 0.10.2 → 0.11.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/lib/cursor.d.ts CHANGED
@@ -2,6 +2,7 @@ export type CursorData = {
2
2
  id: string;
3
3
  ts?: number;
4
4
  values?: Record<string, unknown>;
5
+ nullKeys?: Array<string>;
5
6
  };
6
7
  export declare function parseCursor(cursor: string): CursorData;
7
8
  export declare function serializeCursor(data: CursorData): string;
package/lib/cursor.js CHANGED
@@ -1 +1,7 @@
1
- import{b64uFromJSON as r,b64uToJSON as e}from"@enkaku/codec";export function parseCursor(r){return e(r)}export function serializeCursor(e){return r(e)}
1
+ import { b64uFromJSON, b64uToJSON } from '@sozai/codec';
2
+ export function parseCursor(cursor) {
3
+ return b64uToJSON(cursor);
4
+ }
5
+ export function serializeCursor(data) {
6
+ return b64uFromJSON(data);
7
+ }
package/lib/definition.js CHANGED
@@ -1 +1,7 @@
1
- import{createGraphStore as r}from"./api.js";import{getGraphMigrations as i}from"./migrations.js";export const graphStoreDefinition={name:"graph",migrations:i,createAPI:r};
1
+ import { createGraphStore } from './api.js';
2
+ import { getGraphMigrations } from './migrations.js';
3
+ export const graphStoreDefinition = {
4
+ name: 'graph',
5
+ migrations: getGraphMigrations,
6
+ createAPI: createGraphStore
7
+ };
package/lib/index.js CHANGED
@@ -1 +1,6 @@
1
- export{createGraphStore}from"./api.js";export{graphStoreDefinition}from"./definition.js";export const GRAPH_STORE="graph";export function getGraphStore(r){return r.getStore("graph")}
1
+ export { createGraphStore } from './api.js';
2
+ export { graphStoreDefinition } from './definition.js';
3
+ export const GRAPH_STORE = 'graph';
4
+ export function getGraphStore(provider) {
5
+ return provider.getStore(GRAPH_STORE);
6
+ }
package/lib/migrations.js CHANGED
@@ -1 +1,62 @@
1
- export function getGraphMigrations(e){let t=e.types,a=e.functions.now;return{"0-init":{async up(e){await e.schema.createTable("kubun_graph_document_attachments").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("data",t.binary,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).execute(),await e.schema.createTable("kubun_graph_document_models").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("version",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("behavior",t.text,e=>e.notNull()).addColumn("unique_fields",t.json).addColumn("interfaces",t.json,e=>e.notNull()).addColumn("schema",t.json,e=>e.notNull()).addColumn("fields_meta",t.json,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).execute(),await e.schema.createTable("kubun_graph_document_model_interfaces").ifNotExists().addColumn("interface_id",t.text,e=>e.notNull().references("kubun_graph_document_models.id")).addColumn("implementation_id",t.text,e=>e.notNull().references("kubun_graph_document_models.id")).addUniqueConstraint("kubun_graph_document_model_interfaces_pkey",["interface_id","implementation_id"]).execute(),await e.schema.createTable("kubun_graph_models").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("name",t.text,e=>e.notNull()).addColumn("aliases",t.json,e=>e.defaultTo("{}").notNull()).addColumn("search",t.json).addColumn("extension_sdl",t.text).addColumn("plugin_config",t.json).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createTable("kubun_graph_document_model_links").ifNotExists().addColumn("graph_model_id",t.text,e=>e.notNull().references("kubun_graph_models.id")).addColumn("document_model_id",t.text,e=>e.notNull().references("kubun_graph_document_models.id")).addUniqueConstraint("kubun_graph_document_model_links_pkey",["graph_model_id","document_model_id"]).execute(),await e.schema.createTable("kubun_graph_user_model_access_defaults").ifNotExists().addColumn("owner_did",t.text,e=>e.notNull()).addColumn("model_id",t.text,e=>e.notNull()).addColumn("permission_type",t.text,e=>e.notNull()).addColumn("access_level",t.text,e=>e.notNull()).addColumn("allowed_dids",t.json).addColumn("allowed_circles",t.json).addColumn("allowed_groups",t.json).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("kubun_graph_user_model_access_defaults_pkey",["owner_did","model_id","permission_type"]).execute(),await e.schema.createIndex("idx_graph_user_model_access_owner_model").on("kubun_graph_user_model_access_defaults").columns(["owner_did","model_id"]).execute(),await e.schema.createTable("kubun_graph_mutation_log").ifNotExists().addColumn("mutation_hash",t.text,e=>e.primaryKey()).addColumn("model_id",t.text,e=>e.notNull()).addColumn("document_id",t.text,e=>e.notNull()).addColumn("author_did",t.text,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("mutation_jwt",t.text,e=>e.notNull()).addColumn("status",t.text,e=>e.notNull().defaultTo("applied")).execute(),await e.schema.createIndex("idx_graph_mutation_log_hlc").on("kubun_graph_mutation_log").column("hlc").execute(),await e.schema.createIndex("idx_graph_mutation_log_document").on("kubun_graph_mutation_log").column("document_id").execute(),await e.schema.createIndex("idx_graph_mutation_log_status").on("kubun_graph_mutation_log").column("status").execute(),await e.schema.createTable("kubun_graph_cluster_models").ifNotExists().addColumn("model_id",t.text,e=>e.notNull().primaryKey()).addColumn("cluster_id",t.text,e=>e.notNull()).addColumn("cluster_index","integer",e=>e.notNull()).execute(),await e.schema.createIndex("idx_graph_cluster_models_cluster").on("kubun_graph_cluster_models").column("cluster_id").execute(),await e.schema.createTable("kubun_graph_clusters").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("definition",t.json,e=>e.notNull()).execute(),await e.schema.createTable("kubun_graph_catalogs").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("owner_did",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("filter_criteria",t.json,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_graph_catalogs_owner").ifNotExists().on("kubun_graph_catalogs").column("owner_did").execute()},async down(e){await e.schema.dropTable("kubun_graph_catalogs").ifExists().execute(),await e.schema.dropTable("kubun_graph_clusters").ifExists().execute(),await e.schema.dropTable("kubun_graph_cluster_models").ifExists().execute(),await e.schema.dropTable("kubun_graph_mutation_log").ifExists().execute(),await e.schema.dropTable("kubun_graph_user_model_access_defaults").ifExists().execute(),await e.schema.dropTable("kubun_graph_document_model_links").ifExists().execute(),await e.schema.dropTable("kubun_graph_models").ifExists().execute(),await e.schema.dropTable("kubun_graph_document_model_interfaces").ifExists().execute(),await e.schema.dropTable("kubun_graph_document_models").ifExists().execute(),await e.schema.dropTable("kubun_graph_document_attachments").ifExists().execute()}}}}
1
+ export function getGraphMigrations(ctx) {
2
+ const t = ctx.types;
3
+ const now = ctx.functions.now;
4
+ const init = {
5
+ async up (db) {
6
+ // Document attachments
7
+ await db.schema.createTable('kubun_graph_document_attachments').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('data', t.binary, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
8
+ // Document models
9
+ 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();
10
+ // Document model interfaces
11
+ 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', [
12
+ 'interface_id',
13
+ 'implementation_id'
14
+ ]).execute();
15
+ // Graph models
16
+ await db.schema.createTable('kubun_graph_models').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('aliases', t.json, (col)=>col.defaultTo('{}').notNull()).addColumn('search', t.json).addColumn('extension_sdl', t.text).addColumn('plugin_config', t.json).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
17
+ // Graph document model links (junction: graph-model ↔ document-model)
18
+ await db.schema.createTable('kubun_graph_document_model_links').ifNotExists().addColumn('graph_model_id', t.text, (col)=>col.notNull().references('kubun_graph_models.id')).addColumn('document_model_id', t.text, (col)=>col.notNull().references('kubun_graph_document_models.id')).addUniqueConstraint('kubun_graph_document_model_links_pkey', [
19
+ 'graph_model_id',
20
+ 'document_model_id'
21
+ ]).execute();
22
+ // User model access defaults
23
+ await db.schema.createTable('kubun_graph_user_model_access_defaults').ifNotExists().addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('model_id', t.text, (col)=>col.notNull()).addColumn('permission_type', t.text, (col)=>col.notNull()).addColumn('access_level', t.text, (col)=>col.notNull()).addColumn('allowed_dids', t.json).addColumn('allowed_circles', t.json).addColumn('allowed_groups', t.json).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).addPrimaryKeyConstraint('kubun_graph_user_model_access_defaults_pkey', [
24
+ 'owner_did',
25
+ 'model_id',
26
+ 'permission_type'
27
+ ]).execute();
28
+ await db.schema.createIndex('idx_graph_user_model_access_owner_model').ifNotExists().on('kubun_graph_user_model_access_defaults').columns([
29
+ 'owner_did',
30
+ 'model_id'
31
+ ]).execute();
32
+ // Mutation log
33
+ await db.schema.createTable('kubun_graph_mutation_log').ifNotExists().addColumn('mutation_hash', t.text, (col)=>col.primaryKey()).addColumn('model_id', t.text, (col)=>col.notNull()).addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('author_did', t.text, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('mutation_jwt', t.text, (col)=>col.notNull()).addColumn('status', t.text, (col)=>col.notNull().defaultTo('applied')).execute();
34
+ await db.schema.createIndex('idx_graph_mutation_log_hlc').ifNotExists().on('kubun_graph_mutation_log').column('hlc').execute();
35
+ await db.schema.createIndex('idx_graph_mutation_log_document').ifNotExists().on('kubun_graph_mutation_log').column('document_id').execute();
36
+ await db.schema.createIndex('idx_graph_mutation_log_status').ifNotExists().on('kubun_graph_mutation_log').column('status').execute();
37
+ // Cluster models (model -> cluster mapping)
38
+ await db.schema.createTable('kubun_graph_cluster_models').ifNotExists().addColumn('model_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('cluster_id', t.text, (col)=>col.notNull()).addColumn('cluster_index', 'integer', (col)=>col.notNull()).execute();
39
+ await db.schema.createIndex('idx_graph_cluster_models_cluster').ifNotExists().on('kubun_graph_cluster_models').column('cluster_id').execute();
40
+ // Cluster definitions
41
+ await db.schema.createTable('kubun_graph_clusters').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('definition', t.json, (col)=>col.notNull()).execute();
42
+ // Catalogs
43
+ await db.schema.createTable('kubun_graph_catalogs').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('description', t.text, (col)=>col.notNull().defaultTo('')).addColumn('filter_criteria', t.json, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
44
+ await db.schema.createIndex('idx_graph_catalogs_owner').ifNotExists().on('kubun_graph_catalogs').column('owner_did').execute();
45
+ },
46
+ async down (db) {
47
+ await db.schema.dropTable('kubun_graph_catalogs').ifExists().execute();
48
+ await db.schema.dropTable('kubun_graph_clusters').ifExists().execute();
49
+ await db.schema.dropTable('kubun_graph_cluster_models').ifExists().execute();
50
+ await db.schema.dropTable('kubun_graph_mutation_log').ifExists().execute();
51
+ await db.schema.dropTable('kubun_graph_user_model_access_defaults').ifExists().execute();
52
+ await db.schema.dropTable('kubun_graph_document_model_links').ifExists().execute();
53
+ await db.schema.dropTable('kubun_graph_models').ifExists().execute();
54
+ await db.schema.dropTable('kubun_graph_document_model_interfaces').ifExists().execute();
55
+ await db.schema.dropTable('kubun_graph_document_models').ifExists().execute();
56
+ await db.schema.dropTable('kubun_graph_document_attachments').ifExists().execute();
57
+ }
58
+ };
59
+ return {
60
+ '0-init': init
61
+ };
62
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Adapter } from '@kubun/db-adapter';
2
2
  import type { AggregateSpec, AnyValueFilter, DocumentFilter, DocumentOrderBy } from '@kubun/protocol';
3
- import type { AliasedExpression, ExpressionBuilder, ExpressionWrapper, SelectQueryBuilder } from 'kysely';
3
+ import { type AliasedExpression, type ExpressionBuilder, type ExpressionWrapper, type SelectQueryBuilder } from 'kysely';
4
4
  import type { ConnectionArguments, Document, GraphTables } from './tables.js';
5
5
  export type CoerceFilterValue = (value: unknown) => unknown;
6
6
  type DocumentExpressionBuilder = ExpressionBuilder<GraphTables, 'k_document'>;
@@ -16,8 +16,14 @@ export declare function applyGroupBySelections(eb: DocumentExpressionBuilder, gr
16
16
  selections: Array<AggregateSelection>;
17
17
  expressions: Array<DocumentExpressionWrapper>;
18
18
  };
19
- export declare function applyPagination(query: DocumentQueryBuilder, args: ConnectionArguments, orderBy?: DocumentOrderBy): [DocumentQueryBuilder, number];
19
+ export declare function applyPagination(query: DocumentQueryBuilder, args: ConnectionArguments, orderBy?: DocumentOrderBy, coerce?: CoerceFilterValue): [DocumentQueryBuilder, number];
20
20
  export declare function applyDocumentFilter(eb: DocumentExpressionBuilder, filter: DocumentFilter, path?: Array<string>, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
21
21
  export declare function applyValueFilter(eb: DocumentExpressionBuilder, keys: Array<string>, filter: AnyValueFilter, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
22
- export declare function applyDocumentOrderBy(queryBuilder: DocumentQueryBuilder, orderBy?: DocumentOrderBy, isReverse?: boolean): [DocumentQueryBuilder, Array<Array<string>> | null];
22
+ export declare function applyDocumentOrderBy(queryBuilder: DocumentQueryBuilder, adapter: Adapter, orderBy?: DocumentOrderBy, isReverse?: boolean): [DocumentQueryBuilder, Array<Array<string>> | null];
23
+ export declare const KNOWN_FIELDS: {
24
+ readonly _createdAt: "created_at";
25
+ readonly _docOwner: "owner";
26
+ };
27
+ export type KnownFieldColumn = (typeof KNOWN_FIELDS)[keyof typeof KNOWN_FIELDS];
28
+ export declare function knownFieldColumn(keys: Array<string>): KnownFieldColumn | undefined;
23
29
  export {};
@@ -1 +1,370 @@
1
- import{parseCursor as e}from"./cursor.js";function t(e=50){return Math.min(e,100)}function r(e,t){let r=e.ref("data","->>");for(let e of t)r=r.key(e);return r}export function aggregateFieldName(e){return e.join(".")}export function applyAggregateSelections(e,t,n){let a=[];for(let l of t){let t=aggregateFieldName(l.keys),o=r(e,l.keys),u=l.numeric?n.numericCast(o):o;switch(l.op){case"sum":a.push(e.fn.sum(u).as(`sum_${t}`));break;case"average":a.push(e.fn.sum(u).as(`sum_${t}`)),a.push(e.fn.count(u).as(`count_${t}`));break;case"min":a.push(e.fn.min(u).as(`min_${t}`));break;case"max":a.push(e.fn.max(u).as(`max_${t}`));break;default:throw Error(`Invalid aggregate op: ${l.op}`)}}return a}export function groupKeyAlias(e){return`key_${e.join(".")}`}export function applyGroupBySelections(e,t){let n=[],a=[];for(let l of t){let t=r(e,l.keys);n.push(t.as(groupKeyAlias(l.keys))),a.push(t)}return{selections:n,expressions:a}}function n(e){let t=[],r=e;do{let[e,n]=Object.entries(r)[0];if(t.push(e),"string"==typeof n)return{keys:t,direction:n};r=n}while(null!=r)throw Error("Could not extract field entry")}function a(e,t=!1){return t?"asc"===e?"desc":"asc":e}export function applyPagination(a,l,o=[]){let{first:u,last:i,before:s,after:c}=l;if(null!=u){let l=t(u);return[function(t,a,l,o){let u=t;if(null!=l){let{id:t,ts:a,values:i}=e(l);null!=a?u=u.where(e=>e.or([e("created_at",">",a),e("created_at","=",a).and("id",">",t)])):null!=i&&(u=u.where(e=>{let a=[];for(let l of o){let o=n(l),u=i[o.keys.join(".")];if(null==u)continue;let s=r(e,o.keys);a.push(e.or([e(s,"asc"===o.direction?">":"<",u),e(s,"=",u).and("id",">",t)]))}return e.and(a)}))}return u.orderBy("created_at","asc").limit(a)}(a,l+1,c,o),l]}if(null!=i){let l=t(i);return[function(t,a,l,o){let u=t;if(null!=l){let{id:t,ts:a,values:i}=e(l);null!=a?u=u.where(e=>e.or([e("created_at","<",a),e("created_at","=",a).and("id","<",t)])):null!=i&&(u=u.where(e=>{let a=[];for(let l of o){let o=n(l),u=i[o.keys.join(".")];if(null==u)continue;let s=r(e,o.keys);a.push(e.or([e(s,"asc"===o.direction?"<":">",u),e(s,"=",u).and("id",">",t)]))}return e.and(a)}))}return u.orderBy("created_at","desc").limit(a)}(a,l+1,s,o),l]}let f=t();return[a.orderBy("created_at","asc").limit(f+1),f]}export function applyDocumentFilter(e,t,r=[],n){let a=Object.entries(t);if(1!==a.length)throw Error("Invalid document filter");let[l,o]=a[0];switch(l){case"where":var u,i,s,c;let f;return u=e,i=o,s=r,c=n,f=Object.entries(i).map(([e,t])=>applyValueFilter(u,[...s,e],t,c)),u.and(f);case"and":return e.and(o.map(t=>applyDocumentFilter(e,t,r,n)));case"or":return e.or(o.map(t=>applyDocumentFilter(e,t,r,n)));case"not":return e.not(applyDocumentFilter(e,o,r,n));default:throw Error(`Invalid document filter type: ${l}`)}}export function applyValueFilter(e,t,n,a){let l=Object.entries(n);if(1!==l.length)throw Error("Invalid value filter");let o=r(e,t),[u,i]=l[0],s=a??(e=>e);switch(u){case"isNull":return e(o,!0===i?"is":"is not",null);case"equalTo":return e(o,"=",s(i));case"notEqualTo":return e(o,"!=",s(i));case"in":return e(o,"in",i.map(s));case"notIn":return e(o,"not in",i.map(s));case"lessThan":return e(o,"<",s(i));case"lessThanOrEqualTo":return e(o,"<=",s(i));case"greaterThan":return e(o,">",s(i));case"greaterThanOrEqualTo":return e(o,">=",s(i));default:throw Error(`Invalid value filter type: ${u}`)}}export function applyDocumentOrderBy(e,t=[],n=!1){if(0===t.length)return[e.orderBy("created_at",a("asc",n)),null];let o=e,u=[];for(let e of t){let[t,i]=function e(t,n,o,u=[]){let i=Object.entries(n);if(1!==i.length)throw Error("Invalid order by field");let[s,c]=i[0],f=[...u,s],p=l[s];return null!=p?[t.orderBy(p,a(c,o)),f]:"string"==typeof c?[t.orderBy(e=>r(e,f),a(c,o)),f]:e(t,c,o,f)}(o,e,n);o=t,u.push(i)}return[o,u]}let l={_createdAt:"created_at",_owner:"owner"};
1
+ import { sql } from 'kysely';
2
+ import { parseCursor } from './cursor.js';
3
+ const DEFAULT_LIMIT = 50;
4
+ const MAX_LIMIT = 100;
5
+ function getLimit(value = DEFAULT_LIMIT) {
6
+ return Math.min(value, MAX_LIMIT);
7
+ }
8
+ function getKeyPath(eb, keys) {
9
+ let keyPath = eb.ref('data', '->>');
10
+ for (const key of keys){
11
+ keyPath = keyPath.key(key);
12
+ }
13
+ return keyPath;
14
+ }
15
+ export function aggregateFieldName(keys) {
16
+ return keys.join('.');
17
+ }
18
+ export function applyAggregateSelections(eb, specs, adapter) {
19
+ const selections = [];
20
+ for (const spec of specs){
21
+ const field = aggregateFieldName(spec.keys);
22
+ const raw = getKeyPath(eb, spec.keys);
23
+ const value = spec.numeric ? adapter.numericCast(raw) : raw;
24
+ switch(spec.op){
25
+ case 'sum':
26
+ selections.push(eb.fn.sum(value).as(`sum_${field}`));
27
+ break;
28
+ case 'average':
29
+ selections.push(eb.fn.sum(value).as(`sum_${field}`));
30
+ selections.push(eb.fn.count(value).as(`count_${field}`));
31
+ break;
32
+ case 'min':
33
+ selections.push(eb.fn.min(value).as(`min_${field}`));
34
+ break;
35
+ case 'max':
36
+ selections.push(eb.fn.max(value).as(`max_${field}`));
37
+ break;
38
+ default:
39
+ throw new Error(`Invalid aggregate op: ${spec.op}`);
40
+ }
41
+ }
42
+ return selections;
43
+ }
44
+ export function groupKeyAlias(keys) {
45
+ return `key_${keys.join('.')}`;
46
+ }
47
+ // Builds the aliased group-key select expressions plus the matching GROUP BY
48
+ // expressions. Grouping is on the raw text extraction (no numeric cast): equality
49
+ // bucketing is correct on text, and the JSON encoder serialises a given value
50
+ // identically across model tables.
51
+ export function applyGroupBySelections(eb, groupBy) {
52
+ const selections = [];
53
+ const expressions = [];
54
+ for (const group of groupBy){
55
+ const raw = getKeyPath(eb, group.keys);
56
+ selections.push(raw.as(groupKeyAlias(group.keys)));
57
+ expressions.push(raw);
58
+ }
59
+ return {
60
+ selections,
61
+ expressions
62
+ };
63
+ }
64
+ function getOrderByFieldEntry(field) {
65
+ const keys = [];
66
+ let current = field;
67
+ do {
68
+ const [key, value] = Object.entries(current)[0];
69
+ keys.push(key);
70
+ if (typeof value === 'string') {
71
+ return {
72
+ keys,
73
+ direction: value
74
+ };
75
+ }
76
+ current = value;
77
+ }while (current != null)
78
+ throw new Error('Could not extract field entry');
79
+ }
80
+ function orderByDirection(direction, isReversed = false) {
81
+ return isReversed ? direction === 'asc' ? 'desc' : 'asc' : direction;
82
+ }
83
+ export function applyPagination(query, args, orderBy = [], coerce) {
84
+ const { first, last, before, after } = args;
85
+ if (first != null) {
86
+ const limit = getLimit(first);
87
+ return [
88
+ applyForwardPagination(query, limit + 1, after, orderBy, coerce),
89
+ limit
90
+ ];
91
+ }
92
+ if (last != null) {
93
+ const limit = getLimit(last);
94
+ return [
95
+ applyBackwardPagination(query, limit + 1, before, orderBy, coerce),
96
+ limit
97
+ ];
98
+ }
99
+ const limit = getLimit();
100
+ return [
101
+ query.limit(limit + 1),
102
+ limit
103
+ ];
104
+ }
105
+ // Resolves an ordered field to the same SQL expression the ORDER BY uses, so the
106
+ // keyset predicate compares against the exact column being sorted. Known fields map
107
+ // to real columns (created_at, owner); everything else is a JSON path extraction.
108
+ function orderByFieldExpression(eb, keys) {
109
+ const knownField = knownFieldColumn(keys);
110
+ if (knownField != null) {
111
+ return eb.ref(knownField);
112
+ }
113
+ return getKeyPath(eb, keys);
114
+ }
115
+ // The term selecting rows that strictly advance past the cursor on this field (prior
116
+ // fields equal). Encodes the null bucket per SQL-standard position:
117
+ // ascending (nulls last): cursor non-null -> f > v OR f IS NULL ; cursor null -> nothing
118
+ // descending (nulls first): cursor non-null -> f < v ; cursor null -> f IS NOT NULL
119
+ function advanceTerm(eb, field) {
120
+ if (field.value == null) {
121
+ // Ascending nulls sort last, so nothing advances past a null cursor on this field.
122
+ return field.ascending ? sql`1 = 0` : eb(field.expr, 'is not', null);
123
+ }
124
+ if (field.ascending) {
125
+ return eb.or([
126
+ eb(field.expr, '>', field.value),
127
+ eb(field.expr, 'is', null)
128
+ ]);
129
+ }
130
+ return eb(field.expr, '<', field.value);
131
+ }
132
+ // The term selecting rows that tie this field with the cursor (continue to the next
133
+ // field). A null cursor value ties with other null rows.
134
+ function equalTerm(eb, field) {
135
+ return field.value == null ? eb(field.expr, 'is', null) : eb(field.expr, '=', field.value);
136
+ }
137
+ // Builds the lexicographic keyset predicate that pairs with an ORDER BY whose terminal
138
+ // column is `id`. For ORDER BY (f1..fn, id) the rows strictly after the cursor are:
139
+ // advance(f1)
140
+ // OR (eq(f1) AND advance(f2))
141
+ // ...
142
+ // OR (eq(f1) AND ... AND eq(fn) AND id idCmp idCursor)
143
+ // The tie-break on `id` guarantees no duplicate or skipped row when ordered values
144
+ // repeat. Null ordered values stay in the chain (tagged via `nullKeys`) and sort in
145
+ // SQL-standard position, so concatenated pages match the full ordered set across the
146
+ // null boundary. `forward` true is a forward scan; false flips every field's effective
147
+ // direction (and thus its null position) for a reversed scan.
148
+ function buildKeysetPredicate(eb, orderBy, values, nullKeys, id, forward, coerce) {
149
+ // Cursor comparison values must be coerced the same way filter values are: a JSON
150
+ // path extraction (`data ->> field`) yields adapter-specific text/integer forms
151
+ // (e.g. 1/0 for booleans on SQLite), so an uncoerced cursor value would misfire and
152
+ // drift pages. Known-column fields (created_at, owner) are not JSON-extracted.
153
+ const c = coerce ?? ((v)=>v);
154
+ const nullSet = new Set(nullKeys);
155
+ const fields = [];
156
+ for (const orderByField of orderBy){
157
+ const entry = getOrderByFieldEntry(orderByField);
158
+ const key = entry.keys.join('.');
159
+ const ascending = entry.direction === 'asc' === forward;
160
+ const isKnownField = knownFieldColumn(entry.keys) != null;
161
+ // A key absent from both buckets (stale or malformed cursor) is treated as the null
162
+ // bucket rather than binding an undefined comparison value.
163
+ const isNull = nullSet.has(key) || !(key in values);
164
+ const rawValue = values[key];
165
+ fields.push({
166
+ expr: orderByFieldExpression(eb, entry.keys),
167
+ value: isNull ? null : isKnownField ? rawValue : c(rawValue),
168
+ ascending
169
+ });
170
+ }
171
+ const idCmp = forward ? '>' : '<';
172
+ const clauses = [];
173
+ for(let level = 0; level < fields.length; level += 1){
174
+ const conjuncts = [];
175
+ for(let prior = 0; prior < level; prior += 1){
176
+ conjuncts.push(equalTerm(eb, fields[prior]));
177
+ }
178
+ conjuncts.push(advanceTerm(eb, fields[level]));
179
+ clauses.push(eb.and(conjuncts));
180
+ }
181
+ const tieBreak = [];
182
+ for (const field of fields){
183
+ tieBreak.push(equalTerm(eb, field));
184
+ }
185
+ tieBreak.push(eb('id', idCmp, id));
186
+ clauses.push(eb.and(tieBreak));
187
+ return eb.or(clauses);
188
+ }
189
+ function applyForwardPagination(queryBuilder, limit, after, orderBy, coerce) {
190
+ let query = queryBuilder;
191
+ if (after != null) {
192
+ const { id, ts, values, nullKeys } = parseCursor(after);
193
+ if (ts != null) {
194
+ query = query.where((eb)=>{
195
+ return eb.or([
196
+ eb('created_at', '>', ts),
197
+ eb('created_at', '=', ts).and('id', '>', id)
198
+ ]);
199
+ });
200
+ } else if (values != null || nullKeys != null) {
201
+ query = query.where((eb)=>buildKeysetPredicate(eb, orderBy, values ?? {}, nullKeys ?? [], id, true, coerce));
202
+ }
203
+ }
204
+ return query.limit(limit);
205
+ }
206
+ function applyBackwardPagination(queryBuilder, limit, before, orderBy, coerce) {
207
+ let query = queryBuilder;
208
+ if (before != null) {
209
+ const { id, ts, values, nullKeys } = parseCursor(before);
210
+ if (ts != null) {
211
+ query = query.where((eb)=>{
212
+ return eb.or([
213
+ eb('created_at', '<', ts),
214
+ eb('created_at', '=', ts).and('id', '<', id)
215
+ ]);
216
+ });
217
+ } else if (values != null || nullKeys != null) {
218
+ query = query.where((eb)=>buildKeysetPredicate(eb, orderBy, values ?? {}, nullKeys ?? [], id, false, coerce));
219
+ }
220
+ }
221
+ return query.limit(limit);
222
+ }
223
+ export function applyDocumentFilter(eb, filter, path = [], coerce) {
224
+ const entries = Object.entries(filter);
225
+ if (entries.length !== 1) {
226
+ throw new Error('Invalid document filter');
227
+ }
228
+ const [type, value] = entries[0];
229
+ switch(type){
230
+ case 'where':
231
+ return applyObjectFilter(eb, value, path, coerce);
232
+ case 'and':
233
+ return eb.and(value.map((filter)=>{
234
+ return applyDocumentFilter(eb, filter, path, coerce);
235
+ }));
236
+ case 'or':
237
+ return eb.or(value.map((filter)=>{
238
+ return applyDocumentFilter(eb, filter, path, coerce);
239
+ }));
240
+ case 'not':
241
+ return eb.not(applyDocumentFilter(eb, value, path, coerce));
242
+ default:
243
+ throw new Error(`Invalid document filter type: ${type}`);
244
+ }
245
+ }
246
+ function applyObjectFilter(eb, filter, path, coerce) {
247
+ const criteria = Object.entries(filter).map(([fieldName, valueFilter])=>{
248
+ // TODO: check if value filter or nested object filter should be applied
249
+ // for nested object, check if embedded or related object
250
+ return applyValueFilter(eb, [
251
+ ...path,
252
+ fieldName
253
+ ], valueFilter, coerce);
254
+ });
255
+ return eb.and(criteria);
256
+ }
257
+ export function applyValueFilter(eb, keys, filter, coerce) {
258
+ const entries = Object.entries(filter);
259
+ if (entries.length !== 1) {
260
+ throw new Error('Invalid value filter');
261
+ }
262
+ const fieldName = getKeyPath(eb, keys);
263
+ const [type, value] = entries[0];
264
+ const c = coerce ?? ((v)=>v);
265
+ switch(type){
266
+ case 'isNull':
267
+ return eb(fieldName, value === true ? 'is' : 'is not', null);
268
+ case 'equalTo':
269
+ return eb(fieldName, '=', c(value));
270
+ case 'notEqualTo':
271
+ return eb(fieldName, '!=', c(value));
272
+ case 'in':
273
+ return eb(fieldName, 'in', value.map(c));
274
+ case 'notIn':
275
+ return eb(fieldName, 'not in', value.map(c));
276
+ case 'lessThan':
277
+ return eb(fieldName, '<', c(value));
278
+ case 'lessThanOrEqualTo':
279
+ return eb(fieldName, '<=', c(value));
280
+ case 'greaterThan':
281
+ return eb(fieldName, '>', c(value));
282
+ case 'greaterThanOrEqualTo':
283
+ return eb(fieldName, '>=', c(value));
284
+ default:
285
+ throw new Error(`Invalid value filter type: ${type}`);
286
+ }
287
+ }
288
+ export function applyDocumentOrderBy(queryBuilder, adapter, orderBy = [], isReverse = false) {
289
+ // `id` is the terminal sort column on every path so the cursor keyset can tie-break
290
+ // on a column that is actually part of the ORDER BY. Without it, repeated ordered
291
+ // values would let pagination duplicate or skip rows. `id` sorts in the scan
292
+ // direction (reversed when isReverse) so the keyset comparator stays consistent.
293
+ const idDirection = orderByDirection('asc', isReverse);
294
+ if (orderBy.length === 0) {
295
+ return [
296
+ queryBuilder.orderBy('created_at', idDirection).orderBy('id', idDirection),
297
+ null
298
+ ];
299
+ }
300
+ let query = queryBuilder;
301
+ const paths = [];
302
+ for (const entry of orderBy){
303
+ const [newQuery, path] = applyOrderByField(query, adapter, entry, isReverse);
304
+ query = newQuery;
305
+ paths.push(path);
306
+ }
307
+ query = query.orderBy('id', idDirection);
308
+ return [
309
+ query,
310
+ paths
311
+ ];
312
+ }
313
+ // Ordered fields that map directly to non-null table columns instead of a JSON-path
314
+ // extraction out of `data`. Keys match the GraphQL orderBy field names; values are the
315
+ // physical column names. Cursor construction must read these from the row column, not
316
+ // from `data`, or the ordered value comes back undefined and the keyset predicate drops
317
+ // every row past the first page.
318
+ export const KNOWN_FIELDS = {
319
+ _createdAt: 'created_at',
320
+ _docOwner: 'owner'
321
+ };
322
+ export function knownFieldColumn(keys) {
323
+ if (keys.length !== 1) return undefined;
324
+ return KNOWN_FIELDS[keys[0]];
325
+ }
326
+ function applyOrderByField(query, adapter, orderBy, isReverse, path = []) {
327
+ const entries = Object.entries(orderBy);
328
+ if (entries.length !== 1) {
329
+ throw new Error('Invalid order by field');
330
+ }
331
+ const [key, value] = entries[0];
332
+ const keys = [
333
+ ...path,
334
+ key
335
+ ];
336
+ const knownField = knownFieldColumn(keys);
337
+ if (knownField != null) {
338
+ // Known columns (created_at, owner) are non-null, so no null positioning is needed.
339
+ return [
340
+ query.orderBy(knownField, orderByDirection(value, isReverse)),
341
+ keys
342
+ ];
343
+ }
344
+ if (typeof value !== 'string') {
345
+ return applyOrderByField(query, adapter, value, isReverse, keys);
346
+ }
347
+ // JSON-extracted fields can be null. Sort them in SQL-standard null position (last on
348
+ // ascending, first on descending) on every adapter via the adapter's null-ordering term.
349
+ const direction = orderByDirection(value, isReverse);
350
+ const probe = adapter.nullOrdering(sql`1`, direction);
351
+ if (probe.kind === 'lead') {
352
+ // Prepend an ascending rank column that buckets nulls last/first, then sort on the
353
+ // field itself. The rank expression embeds the field's own JSON-path extraction.
354
+ return [
355
+ query.orderBy((eb)=>adapterLeadExpression(adapter, eb, keys, direction), 'asc').orderBy((eb)=>getKeyPath(eb, keys), direction),
356
+ keys
357
+ ];
358
+ }
359
+ return [
360
+ query.orderBy((eb)=>getKeyPath(eb, keys), (ob)=>direction === 'asc' ? ob.asc().nullsLast() : ob.desc().nullsFirst()),
361
+ keys
362
+ ];
363
+ }
364
+ function adapterLeadExpression(adapter, eb, keys, direction) {
365
+ const term = adapter.nullOrdering(getKeyPath(eb, keys), direction);
366
+ if (term.kind !== 'lead') {
367
+ throw new Error('Expected a leading null-ordering term');
368
+ }
369
+ return term.expression;
370
+ }
package/lib/tables.d.ts CHANGED
@@ -10,6 +10,7 @@ export type DocumentTable<Data extends DocumentData = DocumentData> = {
10
10
  model: string;
11
11
  data: JSONValueColumn<Data> | null;
12
12
  field_hlcs: JSONValueColumn<Record<string, string>> | null;
13
+ field_values: JSONValueColumn<Record<string, unknown>> | null;
13
14
  unique: Uint8Array;
14
15
  created_at: CreatedAtColumn;
15
16
  updated_at: UpdatedAtColumn;
package/lib/tables.js CHANGED
@@ -1 +1 @@
1
- export{};
1
+ export { };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/store-graph",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "license": "see LICENSE.md",
5
5
  "keywords": [],
6
6
  "type": "module",
@@ -15,21 +15,21 @@
15
15
  ],
16
16
  "sideEffects": false,
17
17
  "dependencies": {
18
- "@enkaku/codec": "^0.16.0",
18
+ "@sozai/codec": "^0.1.0",
19
19
  "kysely": "^0.29.2",
20
- "@kubun/db": "^0.10.0",
21
- "@kubun/id": "^0.10.0",
22
- "@kubun/db-adapter": "^0.10.0",
23
- "@kubun/protocol": "^0.10.0"
20
+ "@kubun/db": "^0.11.0",
21
+ "@kubun/db-adapter": "^0.11.0",
22
+ "@kubun/id": "^0.11.0",
23
+ "@kubun/protocol": "^0.11.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@testcontainers/postgresql": "^12.0.1",
27
- "@kubun/db-better-sqlite": "^0.10.0",
28
- "@kubun/db-postgres": "^0.10.0"
26
+ "@testcontainers/postgresql": "^12.0.4",
27
+ "@kubun/db-better-sqlite": "^0.11.0",
28
+ "@kubun/db-postgres": "^0.11.0"
29
29
  },
30
30
  "scripts": {
31
31
  "build:clean": "del lib",
32
- "build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
32
+ "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
33
33
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
34
34
  "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
35
35
  "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",