@kubun/store-graph 0.10.2 → 0.12.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/api.d.ts +83 -3
- package/lib/api.js +1209 -1
- package/lib/cursor.d.ts +1 -0
- package/lib/cursor.js +7 -1
- package/lib/definition.js +7 -1
- package/lib/errors.d.ts +34 -0
- package/lib/errors.js +45 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +7 -1
- package/lib/migrations.d.ts +13 -0
- package/lib/migrations.js +100 -1
- package/lib/query-builder.d.ts +9 -3
- package/lib/query-builder.js +370 -1
- package/lib/tables.d.ts +43 -0
- package/lib/tables.js +1 -1
- package/package.json +18 -18
package/lib/cursor.d.ts
CHANGED
package/lib/cursor.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
import{b64uFromJSON
|
|
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
|
|
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/errors.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `err` is the driver's way of saying `tableName` does not exist.
|
|
3
|
+
* Each adapter words it differently, so the match is the only place the two
|
|
4
|
+
* spellings are known.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isMissingTableError(err: unknown, tableName: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when a read names a document model this device never deployed. The
|
|
9
|
+
* model's table is created at deploy time, so its absence is a fact about this
|
|
10
|
+
* device rather than about the document — a peer holding the model reads the
|
|
11
|
+
* same id fine.
|
|
12
|
+
*
|
|
13
|
+
* Tagged with the protocol-level error code `KB15` so a caller branches on the
|
|
14
|
+
* class instead of matching a driver's wording, which differs per adapter. Not
|
|
15
|
+
* a permanent apply error: a model may be deployed later, so a mutation naming
|
|
16
|
+
* one stays retryable.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ModelNotDeployedError extends Error {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(modelID: string, options?: {
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
get code(): 'KB15';
|
|
24
|
+
get modelID(): string;
|
|
25
|
+
/**
|
|
26
|
+
* graphql-js copies a wrapped error's `extensions` onto the error it locates,
|
|
27
|
+
* so a resolver that lets this through surfaces the code to the app without
|
|
28
|
+
* translating it at the throw site.
|
|
29
|
+
*/
|
|
30
|
+
get extensions(): {
|
|
31
|
+
code: 'KB15';
|
|
32
|
+
modelID: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `err` is the driver's way of saying `tableName` does not exist.
|
|
3
|
+
* Each adapter words it differently, so the match is the only place the two
|
|
4
|
+
* spellings are known.
|
|
5
|
+
*/ export function isMissingTableError(err, tableName) {
|
|
6
|
+
if (!(err instanceof Error)) return false;
|
|
7
|
+
const message = err.message;
|
|
8
|
+
// SQLite: "no such table: k_<modelID>"
|
|
9
|
+
// Postgres: `relation "k_<modelID>" does not exist`
|
|
10
|
+
return message.includes('no such table') && message.includes(tableName) || message.includes('does not exist') && message.includes(tableName);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when a read names a document model this device never deployed. The
|
|
14
|
+
* model's table is created at deploy time, so its absence is a fact about this
|
|
15
|
+
* device rather than about the document — a peer holding the model reads the
|
|
16
|
+
* same id fine.
|
|
17
|
+
*
|
|
18
|
+
* Tagged with the protocol-level error code `KB15` so a caller branches on the
|
|
19
|
+
* class instead of matching a driver's wording, which differs per adapter. Not
|
|
20
|
+
* a permanent apply error: a model may be deployed later, so a mutation naming
|
|
21
|
+
* one stays retryable.
|
|
22
|
+
*/ export class ModelNotDeployedError extends Error {
|
|
23
|
+
#modelID;
|
|
24
|
+
constructor(modelID, options){
|
|
25
|
+
super(`Document model ${modelID} is not deployed on this device`, options);
|
|
26
|
+
this.name = 'ModelNotDeployedError';
|
|
27
|
+
this.#modelID = modelID;
|
|
28
|
+
}
|
|
29
|
+
get code() {
|
|
30
|
+
return 'KB15';
|
|
31
|
+
}
|
|
32
|
+
get modelID() {
|
|
33
|
+
return this.#modelID;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* graphql-js copies a wrapped error's `extensions` onto the error it locates,
|
|
37
|
+
* so a resolver that lets this through surfaces the code to the app without
|
|
38
|
+
* translating it at the throw site.
|
|
39
|
+
*/ get extensions() {
|
|
40
|
+
return {
|
|
41
|
+
code: 'KB15',
|
|
42
|
+
modelID: this.#modelID
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { StoreProvider } from '@kubun/db';
|
|
2
2
|
import type { GraphStoreAPI } from './api.js';
|
|
3
|
-
export type { GraphStoreAPI, StoredAccessRule } from './api.js';
|
|
3
|
+
export type { GraphStoreAPI, StoredAccessDefaultRow, StoredAccessRule } from './api.js';
|
|
4
4
|
export { createGraphStore } from './api.js';
|
|
5
5
|
export { graphStoreDefinition } from './definition.js';
|
|
6
|
+
export { ModelNotDeployedError } from './errors.js';
|
|
6
7
|
export declare const GRAPH_STORE: "graph";
|
|
7
8
|
export declare function getGraphStore(provider: StoreProvider): Promise<GraphStoreAPI>;
|
|
8
9
|
export type { AccessLevel, AccessPermissions, AddDocumentModelParams, Catalog, ClusterDefinitionRow, ClusterModel, ConnectionArguments, CreateDocumentParams, CreateGraphParams, CursorDocument, Document, DocumentAttachment, DocumentData, DocumentModelRow, DocumentParams, GraphModel, GraphModelWithRecord, GraphTables, InsertCatalog, InsertClusterModel, InsertDocument, InsertDocumentAttachment, InsertDocumentModel, InsertDocumentModelInterface, InsertMutationLogEntry, ListDocumentsParams, MutationLogEntry, PaginatedResult, PaginationParams, QueryDocumentsParams, QueryDocumentsResult, SaveDocumentParams, SearchConfig, SearchDocumentResult, SearchDocumentsParams, UpdateDocument, UpdateDocumentModel, ViewerReadAccess, } from './tables.js';
|
package/lib/index.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
export{createGraphStore}from
|
|
1
|
+
export { createGraphStore } from './api.js';
|
|
2
|
+
export { graphStoreDefinition } from './definition.js';
|
|
3
|
+
export { ModelNotDeployedError } from './errors.js';
|
|
4
|
+
export const GRAPH_STORE = 'graph';
|
|
5
|
+
export function getGraphStore(provider) {
|
|
6
|
+
return provider.getStore(GRAPH_STORE);
|
|
7
|
+
}
|
package/lib/migrations.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
import type { MigrationContext } from '@kubun/db';
|
|
2
2
|
import type { Migration } from 'kysely/migration';
|
|
3
|
+
/**
|
|
4
|
+
* A shipped migration is immutable: once a database has run it, editing it
|
|
5
|
+
* changes nothing on that database and silently diverges the schema from the
|
|
6
|
+
* code. Every schema change after a release therefore goes in a NEW migration,
|
|
7
|
+
* which is why the later ones here say so.
|
|
8
|
+
*
|
|
9
|
+
* `0-init` is a deliberate exception on this change: it was edited in place
|
|
10
|
+
* rather than extended. That is a BREAKING change — a database created before it
|
|
11
|
+
* will not pick the new columns up and must be recreated. It was taken because
|
|
12
|
+
* this release already requires recreating local databases for unrelated reasons,
|
|
13
|
+
* so folding the change into the initial schema costs nothing and keeps `0-init`
|
|
14
|
+
* readable as the whole schema. Do NOT repeat it after this release ships.
|
|
15
|
+
*/
|
|
3
16
|
export declare function getGraphMigrations(ctx: MigrationContext): Record<string, Migration>;
|
package/lib/migrations.js
CHANGED
|
@@ -1 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A shipped migration is immutable: once a database has run it, editing it
|
|
3
|
+
* changes nothing on that database and silently diverges the schema from the
|
|
4
|
+
* code. Every schema change after a release therefore goes in a NEW migration,
|
|
5
|
+
* which is why the later ones here say so.
|
|
6
|
+
*
|
|
7
|
+
* `0-init` is a deliberate exception on this change: it was edited in place
|
|
8
|
+
* rather than extended. That is a BREAKING change — a database created before it
|
|
9
|
+
* will not pick the new columns up and must be recreated. It was taken because
|
|
10
|
+
* this release already requires recreating local databases for unrelated reasons,
|
|
11
|
+
* so folding the change into the initial schema costs nothing and keeps `0-init`
|
|
12
|
+
* readable as the whole schema. Do NOT repeat it after this release ships.
|
|
13
|
+
*/ export function getGraphMigrations(ctx) {
|
|
14
|
+
const t = ctx.types;
|
|
15
|
+
const now = ctx.functions.now;
|
|
16
|
+
const init = {
|
|
17
|
+
async up (db) {
|
|
18
|
+
// Document attachments
|
|
19
|
+
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();
|
|
20
|
+
// Document models
|
|
21
|
+
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();
|
|
22
|
+
// Document model interfaces
|
|
23
|
+
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', [
|
|
24
|
+
'interface_id',
|
|
25
|
+
'implementation_id'
|
|
26
|
+
]).execute();
|
|
27
|
+
// Graph models
|
|
28
|
+
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();
|
|
29
|
+
// Graph document model links (junction: graph-model ↔ document-model)
|
|
30
|
+
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', [
|
|
31
|
+
'graph_model_id',
|
|
32
|
+
'document_model_id'
|
|
33
|
+
]).execute();
|
|
34
|
+
// User model access defaults
|
|
35
|
+
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)// LWW anchor for replicated rules; null for rows never stamped by a broadcast.
|
|
36
|
+
.addColumn('hlc', t.text)// Retained-tombstone marker (0/null = active, 1 = removed): removal keeps
|
|
37
|
+
// the row so its `hlc` stays the LWW anchor and hides it from reads.
|
|
38
|
+
.addColumn('removed', 'integer').addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).addPrimaryKeyConstraint('kubun_graph_user_model_access_defaults_pkey', [
|
|
39
|
+
'owner_did',
|
|
40
|
+
'model_id',
|
|
41
|
+
'permission_type'
|
|
42
|
+
]).execute();
|
|
43
|
+
await db.schema.createIndex('idx_graph_user_model_access_owner_model').ifNotExists().on('kubun_graph_user_model_access_defaults').columns([
|
|
44
|
+
'owner_did',
|
|
45
|
+
'model_id'
|
|
46
|
+
]).execute();
|
|
47
|
+
// Mutation log
|
|
48
|
+
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();
|
|
49
|
+
await db.schema.createIndex('idx_graph_mutation_log_hlc').ifNotExists().on('kubun_graph_mutation_log').column('hlc').execute();
|
|
50
|
+
await db.schema.createIndex('idx_graph_mutation_log_document').ifNotExists().on('kubun_graph_mutation_log').column('document_id').execute();
|
|
51
|
+
await db.schema.createIndex('idx_graph_mutation_log_status').ifNotExists().on('kubun_graph_mutation_log').column('status').execute();
|
|
52
|
+
// Cluster models (model -> cluster mapping)
|
|
53
|
+
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();
|
|
54
|
+
await db.schema.createIndex('idx_graph_cluster_models_cluster').ifNotExists().on('kubun_graph_cluster_models').column('cluster_id').execute();
|
|
55
|
+
// Cluster definitions
|
|
56
|
+
await db.schema.createTable('kubun_graph_clusters').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('definition', t.json, (col)=>col.notNull()).execute();
|
|
57
|
+
// Catalogs
|
|
58
|
+
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())// Local-only sync activation (1 = active, 0/null = known-not-activated).
|
|
59
|
+
.addColumn('active', 'integer')// First-discovery provenance (nullable; never overwritten on re-discovery).
|
|
60
|
+
.addColumn('source_group_id', t.text).addColumn('source_circle_id', t.text)// Creator-signed `catalog:set` token; forwarded verbatim in invite seeds.
|
|
61
|
+
.addColumn('signed_token', t.text).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
|
|
62
|
+
await db.schema.createIndex('idx_graph_catalogs_owner').ifNotExists().on('kubun_graph_catalogs').column('owner_did').execute();
|
|
63
|
+
// `.ifNotExists()` protects a half-created table from being recreated —
|
|
64
|
+
// and, because this migration was edited in place, it equally protects a
|
|
65
|
+
// table an EARLIER run created from ever acquiring the columns that edit
|
|
66
|
+
// added. A database that ran the pre-edit `0-init`, or crashed after
|
|
67
|
+
// creating this table but before Kysely recorded the migration (SQLite
|
|
68
|
+
// DDL is not transactional), would otherwise complete this migration with
|
|
69
|
+
// a table that has no `hlc` and no `removed` — and every subsequent
|
|
70
|
+
// `setUserModelAccessDefault` would fail with "no such column: hlc",
|
|
71
|
+
// permanently, with no repair path. Healing it here is what makes the
|
|
72
|
+
// half-upgraded shape unrepresentable rather than merely unlikely.
|
|
73
|
+
const accessTable = (await db.introspection.getTables()).find((table)=>table.name === 'kubun_graph_user_model_access_defaults');
|
|
74
|
+
if (accessTable != null) {
|
|
75
|
+
const existing = new Set(accessTable.columns.map((column)=>column.name));
|
|
76
|
+
if (!existing.has('hlc')) {
|
|
77
|
+
await db.schema.alterTable('kubun_graph_user_model_access_defaults').addColumn('hlc', t.text).execute();
|
|
78
|
+
}
|
|
79
|
+
if (!existing.has('removed')) {
|
|
80
|
+
await db.schema.alterTable('kubun_graph_user_model_access_defaults').addColumn('removed', 'integer').execute();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async down (db) {
|
|
85
|
+
await db.schema.dropTable('kubun_graph_catalogs').ifExists().execute();
|
|
86
|
+
await db.schema.dropTable('kubun_graph_clusters').ifExists().execute();
|
|
87
|
+
await db.schema.dropTable('kubun_graph_cluster_models').ifExists().execute();
|
|
88
|
+
await db.schema.dropTable('kubun_graph_mutation_log').ifExists().execute();
|
|
89
|
+
await db.schema.dropTable('kubun_graph_user_model_access_defaults').ifExists().execute();
|
|
90
|
+
await db.schema.dropTable('kubun_graph_document_model_links').ifExists().execute();
|
|
91
|
+
await db.schema.dropTable('kubun_graph_models').ifExists().execute();
|
|
92
|
+
await db.schema.dropTable('kubun_graph_document_model_interfaces').ifExists().execute();
|
|
93
|
+
await db.schema.dropTable('kubun_graph_document_models').ifExists().execute();
|
|
94
|
+
await db.schema.dropTable('kubun_graph_document_attachments').ifExists().execute();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
'0-init': init
|
|
99
|
+
};
|
|
100
|
+
}
|
package/lib/query-builder.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|