@kubun/store-graph 0.11.0 → 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 +81 -2
- package/lib/api.js +241 -25
- package/lib/errors.d.ts +34 -0
- package/lib/errors.js +45 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -0
- package/lib/migrations.d.ts +13 -0
- package/lib/migrations.js +41 -3
- package/lib/tables.d.ts +42 -0
- package/package.json +17 -17
package/lib/api.d.ts
CHANGED
|
@@ -12,8 +12,36 @@ export type StoredAccessRule = {
|
|
|
12
12
|
allowedCircles: Array<string> | null;
|
|
13
13
|
allowedGroups: Array<string> | null;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* A single stored access-default row, tagged with the model and permission
|
|
17
|
+
* type it belongs to. Returned by `listUserModelAccessDefaults`, which -
|
|
18
|
+
* unlike `getUserModelAccessDefault` - is not scoped to one (model,
|
|
19
|
+
* permissionType) pair.
|
|
20
|
+
*/
|
|
21
|
+
export type StoredAccessDefaultRow = StoredAccessRule & {
|
|
22
|
+
modelID: string;
|
|
23
|
+
permissionType: 'read' | 'write';
|
|
24
|
+
/**
|
|
25
|
+
* The row's LWW anchor, or null when written without a stamp. Carried here so
|
|
26
|
+
* a caller needing rule + anchor for several models reads one list rather than
|
|
27
|
+
* a `getUserModelAccessDefault` / `getUserModelAccessDefaultHLC` pair each.
|
|
28
|
+
*/
|
|
29
|
+
hlc: string | null;
|
|
30
|
+
};
|
|
15
31
|
export type GraphStoreAPI = {
|
|
16
32
|
getDocument(id: DocumentID | string): Promise<DocumentNode | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Batch fetch documents, returning a Map keyed by the document ID string. A
|
|
35
|
+
* missing id is absent from the map, exactly as `getDocument` returning `null`.
|
|
36
|
+
*
|
|
37
|
+
* Documents live in a per-model table, so the reads are grouped by model and
|
|
38
|
+
* cost one query per DISTINCT model rather than one per id. A model with no
|
|
39
|
+
* table contributes nothing, matching `getDocument`'s behaviour for an id whose
|
|
40
|
+
* model was never deployed. Empty input short-circuits with no SQL.
|
|
41
|
+
*
|
|
42
|
+
* Caller is responsible for deduplicating ids.
|
|
43
|
+
*/
|
|
44
|
+
getDocuments(ids: Array<DocumentID | string>): Promise<Map<string, DocumentNode>>;
|
|
17
45
|
getDocumentModel(id: DocumentModelID | string): Promise<DocumentModel>;
|
|
18
46
|
/**
|
|
19
47
|
* Return the interface model IDs declared by `modelID` from the
|
|
@@ -61,7 +89,30 @@ export type GraphStoreAPI = {
|
|
|
61
89
|
updateSearchEntry(modelID: string, documentID: string, data: Record<string, unknown>, fields: Array<string>): Promise<void>;
|
|
62
90
|
removeSearchEntry(modelID: string, documentID: string): Promise<void>;
|
|
63
91
|
searchDocuments(params: SearchDocumentsParams): Promise<Array<SearchDocumentResult>>;
|
|
92
|
+
/** Create an own catalog: stored active (scopes sync) with null provenance. */
|
|
64
93
|
createCatalog(catalog: InsertCatalog): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Insert a discovered catalog as known (not active) with first-discovery
|
|
96
|
+
* provenance. Idempotent on the id: an already-stored catalog is left
|
|
97
|
+
* untouched, so its local `active` flag and first-discovery source columns
|
|
98
|
+
* are never overwritten by a later re-discovery.
|
|
99
|
+
*/
|
|
100
|
+
upsertDiscoveredCatalog(params: {
|
|
101
|
+
catalog: InsertCatalog;
|
|
102
|
+
sourceGroupID: string | null;
|
|
103
|
+
sourceCircleID: string | null;
|
|
104
|
+
}): Promise<void>;
|
|
105
|
+
/** Toggle a catalog's local sync activation. */
|
|
106
|
+
setCatalogActive(id: string, active: boolean): Promise<void>;
|
|
107
|
+
/** Flip several catalogs together — one statement, not one per id. */
|
|
108
|
+
setCatalogsActive(ids: Array<string>, active: boolean): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* All locally-stored catalog rows regardless of owner, optionally filtered by
|
|
111
|
+
* activation state. Backs the viewer's known-catalogs surface.
|
|
112
|
+
*/
|
|
113
|
+
listStoredCatalogs(filter?: {
|
|
114
|
+
active?: boolean;
|
|
115
|
+
}): Promise<Array<Catalog>>;
|
|
65
116
|
getCatalog(id: string): Promise<Catalog | undefined>;
|
|
66
117
|
/**
|
|
67
118
|
* Batch fetch catalogs by ID. Returns a Map keyed by ID; missing IDs are
|
|
@@ -71,7 +122,7 @@ export type GraphStoreAPI = {
|
|
|
71
122
|
* Caller is responsible for deduplicating IDs (e.g. via `Array.from(new Set(...))`).
|
|
72
123
|
*/
|
|
73
124
|
getCatalogs(ids: Array<string>): Promise<Map<string, Catalog>>;
|
|
74
|
-
updateCatalog(id: string, update: Partial<Pick<InsertCatalog, 'name' | 'description' | 'filter_criteria' | 'hlc'>>): Promise<void>;
|
|
125
|
+
updateCatalog(id: string, update: Partial<Pick<InsertCatalog, 'name' | 'description' | 'filter_criteria' | 'hlc' | 'signed_token'>>): Promise<void>;
|
|
75
126
|
deleteCatalog(id: string): Promise<void>;
|
|
76
127
|
listCatalogs(ownerDID: string): Promise<Array<Catalog>>;
|
|
77
128
|
resolveCatalogScope(catalogID: string): Promise<{
|
|
@@ -79,7 +130,22 @@ export type GraphStoreAPI = {
|
|
|
79
130
|
owners: Array<string> | undefined;
|
|
80
131
|
}>;
|
|
81
132
|
registerClusterModels(clusterID: string, entries: Array<InsertClusterModel>): Promise<void>;
|
|
133
|
+
/** A tombstoned (removed) rule reads as absent. */
|
|
82
134
|
getUserModelAccessDefault(ownerDID: string, modelID: string, permissionType: 'read' | 'write'): Promise<StoredAccessRule | null>;
|
|
135
|
+
/**
|
|
136
|
+
* All active stored access-default rows for `ownerDID`, across every model and
|
|
137
|
+
* permission type. Tombstoned (removed) rows are excluded. Bounded by the
|
|
138
|
+
* owner's model count, no pagination.
|
|
139
|
+
*/
|
|
140
|
+
listUserModelAccessDefaults(ownerDID: string): Promise<Array<StoredAccessDefaultRow>>;
|
|
141
|
+
/**
|
|
142
|
+
* The LWW anchor (`hlc`) stamped on a stored rule, or null when the rule is
|
|
143
|
+
* absent or was written without a stamp (a legacy or unsynced local policy).
|
|
144
|
+
* A caller applying a replicated rule reads this to reject an older write.
|
|
145
|
+
* A tombstoned row keeps its anchor: this returns the removal's `hlc` so a
|
|
146
|
+
* stale set arriving after the remove is still rejected.
|
|
147
|
+
*/
|
|
148
|
+
getUserModelAccessDefaultHLC(ownerDID: string, modelID: string, permissionType: 'read' | 'write'): Promise<string | null>;
|
|
83
149
|
setUserModelAccessDefault(params: {
|
|
84
150
|
ownerDID: string;
|
|
85
151
|
modelID: string;
|
|
@@ -88,8 +154,21 @@ export type GraphStoreAPI = {
|
|
|
88
154
|
allowedDIDs: Array<string> | null;
|
|
89
155
|
allowedCircles: Array<string> | null;
|
|
90
156
|
allowedGroups: Array<string> | null;
|
|
157
|
+
/** LWW anchor. Omitted by callers that do not participate in replication. */
|
|
158
|
+
hlc?: string;
|
|
91
159
|
}): Promise<void>;
|
|
92
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Tombstone the given permission types: retain each row with its rule fields
|
|
162
|
+
* nulled, `removed` set, and `hlc` stamped as the LWW anchor. The row is kept
|
|
163
|
+
* (not deleted) so a stale set arriving after the removal is rejected by HLC.
|
|
164
|
+
* A type with no existing row is skipped (nothing to tombstone).
|
|
165
|
+
*/
|
|
166
|
+
removeUserModelAccessDefaults(ownerDID: string, modelID: string, permissionTypes: Array<string>, hlc: string): Promise<void>;
|
|
167
|
+
/**
|
|
168
|
+
* Document IDs across every scope, one query per distinct model. Repeating a
|
|
169
|
+
* (model, owner) pair yields its documents once — the result is a set, and the
|
|
170
|
+
* caller builds a Merkle tree from it, where a duplicate would be a defect.
|
|
171
|
+
*/
|
|
93
172
|
getDocumentIDsForScope(scopes: Array<{
|
|
94
173
|
modelID: string;
|
|
95
174
|
ownerDID: string;
|
package/lib/api.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { DocumentID, DocumentModelID } from '@kubun/id';
|
|
2
2
|
import { serializeCursor } from './cursor.js';
|
|
3
|
-
|
|
4
|
-
if (!(err instanceof Error)) return false;
|
|
5
|
-
const message = err.message;
|
|
6
|
-
// SQLite: "no such table: k_<modelID>"
|
|
7
|
-
// Postgres: `relation "k_<modelID>" does not exist`
|
|
8
|
-
return message.includes('no such table') && message.includes(tableName) || message.includes('does not exist') && message.includes(tableName);
|
|
9
|
-
}
|
|
3
|
+
import { isMissingTableError, ModelNotDeployedError } from './errors.js';
|
|
10
4
|
import { aggregateFieldName, applyAggregateSelections, applyDocumentFilter, applyDocumentOrderBy, applyGroupBySelections, applyPagination, groupKeyAlias, knownFieldColumn } from './query-builder.js';
|
|
11
5
|
// --- Helper functions ---
|
|
12
6
|
// SQL numeric results round-trip as strings on some adapters (Postgres `numeric`/`sum`),
|
|
@@ -352,9 +346,62 @@ export function createGraphStore(db, adapter) {
|
|
|
352
346
|
// --- Document operations ---
|
|
353
347
|
async getDocument (id) {
|
|
354
348
|
const parsedID = DocumentID.from(id);
|
|
355
|
-
const
|
|
349
|
+
const modelID = parsedID.model.toString();
|
|
350
|
+
const doc = await db.selectFrom(`k_${modelID}`).selectAll().where('id', '=', parsedID.toString()).executeTakeFirst().catch((err)=>{
|
|
351
|
+
// A single-document read names its model, so an undeployed one is a
|
|
352
|
+
// distinct answer from an absent document and not an empty result:
|
|
353
|
+
// the id may well exist on a peer that holds the model.
|
|
354
|
+
if (isMissingTableError(err, `k_${modelID}`)) {
|
|
355
|
+
throw new ModelNotDeployedError(modelID, {
|
|
356
|
+
cause: err
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
throw err;
|
|
360
|
+
});
|
|
356
361
|
return doc ? documentNodeFromTable(adapter, doc) : null;
|
|
357
362
|
},
|
|
363
|
+
async getDocuments (ids) {
|
|
364
|
+
const result = new Map();
|
|
365
|
+
if (ids.length === 0) return result;
|
|
366
|
+
// Group by model so each model's table is read once with every id asked
|
|
367
|
+
// for, rather than once per id.
|
|
368
|
+
const idsByModel = new Map();
|
|
369
|
+
for (const id of ids){
|
|
370
|
+
const parsedID = DocumentID.from(id);
|
|
371
|
+
const modelID = parsedID.model.toString();
|
|
372
|
+
const modelIDs = idsByModel.get(modelID);
|
|
373
|
+
if (modelIDs == null) {
|
|
374
|
+
idsByModel.set(modelID, [
|
|
375
|
+
parsedID.toString()
|
|
376
|
+
]);
|
|
377
|
+
} else {
|
|
378
|
+
modelIDs.push(parsedID.toString());
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// One read per model, all issued together: they touch disjoint tables, so
|
|
382
|
+
// a pooled adapter overlaps them and a serial one costs what it did. The
|
|
383
|
+
// rows are folded in `idsByModel` order afterwards, so what wins a
|
|
384
|
+
// duplicate id does not depend on which query returned first.
|
|
385
|
+
const rowsByModel = await Promise.all(Array.from(idsByModel, async ([modelID, documentIDs])=>{
|
|
386
|
+
try {
|
|
387
|
+
return await db.selectFrom(`k_${modelID}`).selectAll().where('id', 'in', documentIDs).execute();
|
|
388
|
+
} catch (err) {
|
|
389
|
+
// A batch spans models, so one undeployed model contributes nothing
|
|
390
|
+
// rather than failing the read for every other id asked for. The
|
|
391
|
+
// single-id read throws `ModelNotDeployedError` instead: there, the
|
|
392
|
+
// undeployed model is the whole answer.
|
|
393
|
+
if (!isMissingTableError(err, `k_${modelID}`)) throw err;
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
}));
|
|
397
|
+
for (const docs of rowsByModel){
|
|
398
|
+
for (const doc of docs){
|
|
399
|
+
const node = documentNodeFromTable(adapter, doc);
|
|
400
|
+
result.set(node.id, node);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
},
|
|
358
405
|
async getDocumentModel (id) {
|
|
359
406
|
const row = await db.selectFrom('kubun_graph_document_models').selectAll().where('id', '=', id.toString()).executeTakeFirstOrThrow();
|
|
360
407
|
return documentModelFromTable(row);
|
|
@@ -748,6 +795,25 @@ export function createGraphStore(db, adapter) {
|
|
|
748
795
|
added.add(id);
|
|
749
796
|
}));
|
|
750
797
|
}
|
|
798
|
+
for (const [clusterID, cluster] of Object.entries(params.clusters ?? {})){
|
|
799
|
+
const entries = Object.entries(cluster.models).map(([modelID, index])=>({
|
|
800
|
+
model_id: modelID,
|
|
801
|
+
cluster_id: clusterID,
|
|
802
|
+
cluster_index: index
|
|
803
|
+
}));
|
|
804
|
+
if (entries.length > 0) {
|
|
805
|
+
await trx.insertInto('kubun_graph_cluster_models').values(entries).onConflict((oc)=>oc.column('model_id').doUpdateSet((eb)=>({
|
|
806
|
+
cluster_id: eb.ref('excluded.cluster_id'),
|
|
807
|
+
cluster_index: eb.ref('excluded.cluster_index')
|
|
808
|
+
}))).execute();
|
|
809
|
+
}
|
|
810
|
+
await trx.insertInto('kubun_graph_clusters').values({
|
|
811
|
+
id: clusterID,
|
|
812
|
+
definition: adapter.encodeJSON(cluster.definition)
|
|
813
|
+
}).onConflict((oc)=>oc.column('id').doUpdateSet((eb)=>({
|
|
814
|
+
definition: eb.ref('excluded.definition')
|
|
815
|
+
}))).execute();
|
|
816
|
+
}
|
|
751
817
|
});
|
|
752
818
|
return graphModelID;
|
|
753
819
|
},
|
|
@@ -852,9 +918,59 @@ export function createGraphStore(db, adapter) {
|
|
|
852
918
|
name: catalog.name,
|
|
853
919
|
description: catalog.description,
|
|
854
920
|
filter_criteria: adapter.encodeJSON(catalog.filter_criteria),
|
|
855
|
-
hlc: catalog.hlc
|
|
921
|
+
hlc: catalog.hlc,
|
|
922
|
+
// Own catalogs scope sync immediately; no discovery provenance.
|
|
923
|
+
active: 1,
|
|
924
|
+
source_group_id: null,
|
|
925
|
+
source_circle_id: null,
|
|
926
|
+
signed_token: catalog.signed_token ?? null
|
|
856
927
|
}).execute();
|
|
857
928
|
},
|
|
929
|
+
async upsertDiscoveredCatalog ({ catalog, sourceGroupID, sourceCircleID }) {
|
|
930
|
+
await db.insertInto('kubun_graph_catalogs').values({
|
|
931
|
+
id: catalog.id,
|
|
932
|
+
owner_did: catalog.owner_did,
|
|
933
|
+
name: catalog.name,
|
|
934
|
+
description: catalog.description,
|
|
935
|
+
filter_criteria: adapter.encodeJSON(catalog.filter_criteria),
|
|
936
|
+
hlc: catalog.hlc,
|
|
937
|
+
// Discovery applies no sync — the catalog arrives known, awaiting an
|
|
938
|
+
// explicit local activation decision.
|
|
939
|
+
active: 0,
|
|
940
|
+
source_group_id: sourceGroupID,
|
|
941
|
+
source_circle_id: sourceCircleID,
|
|
942
|
+
signed_token: catalog.signed_token ?? null
|
|
943
|
+
})// First-discovery-wins: an existing row (already known or locally
|
|
944
|
+
// activated) keeps its `active` flag and source columns untouched.
|
|
945
|
+
.onConflict((oc)=>oc.column('id').doNothing()).execute();
|
|
946
|
+
},
|
|
947
|
+
async setCatalogActive (id, active) {
|
|
948
|
+
await db.updateTable('kubun_graph_catalogs').set({
|
|
949
|
+
active: active ? 1 : 0,
|
|
950
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
951
|
+
}).where('id', '=', id).execute();
|
|
952
|
+
},
|
|
953
|
+
async setCatalogsActive (ids, active) {
|
|
954
|
+
if (ids.length === 0) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
await db.updateTable('kubun_graph_catalogs').set({
|
|
958
|
+
active: active ? 1 : 0,
|
|
959
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
960
|
+
}).where('id', 'in', ids).execute();
|
|
961
|
+
},
|
|
962
|
+
async listStoredCatalogs (filter) {
|
|
963
|
+
let query = db.selectFrom('kubun_graph_catalogs').selectAll();
|
|
964
|
+
if (filter?.active === true) {
|
|
965
|
+
query = query.where('active', '=', 1);
|
|
966
|
+
} else if (filter?.active === false) {
|
|
967
|
+
query = query.where((eb)=>eb.or([
|
|
968
|
+
eb('active', 'is', null),
|
|
969
|
+
eb('active', '=', 0)
|
|
970
|
+
]));
|
|
971
|
+
}
|
|
972
|
+
return await query.execute();
|
|
973
|
+
},
|
|
858
974
|
async getCatalog (id) {
|
|
859
975
|
return await db.selectFrom('kubun_graph_catalogs').selectAll().where('id', '=', id).executeTakeFirst();
|
|
860
976
|
},
|
|
@@ -875,6 +991,7 @@ export function createGraphStore(db, adapter) {
|
|
|
875
991
|
values.filter_criteria = adapter.encodeJSON(update.filter_criteria);
|
|
876
992
|
}
|
|
877
993
|
if (update.hlc != null) values.hlc = update.hlc;
|
|
994
|
+
if (update.signed_token != null) values.signed_token = update.signed_token;
|
|
878
995
|
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
879
996
|
await db.updateTable('kubun_graph_catalogs').set(values).where('id', '=', id).execute();
|
|
880
997
|
},
|
|
@@ -922,9 +1039,12 @@ export function createGraphStore(db, adapter) {
|
|
|
922
1039
|
'access_level',
|
|
923
1040
|
'allowed_dids',
|
|
924
1041
|
'allowed_circles',
|
|
925
|
-
'allowed_groups'
|
|
1042
|
+
'allowed_groups',
|
|
1043
|
+
'removed'
|
|
926
1044
|
]).where('owner_did', '=', ownerDID).where('model_id', '=', modelID).where('permission_type', '=', permissionType).executeTakeFirst();
|
|
927
|
-
|
|
1045
|
+
// A tombstoned row reads as absent so a removed rule falls back to the
|
|
1046
|
+
// server default rather than surfacing a nulled rule.
|
|
1047
|
+
if (!result || result.removed) {
|
|
928
1048
|
return null;
|
|
929
1049
|
}
|
|
930
1050
|
return {
|
|
@@ -934,6 +1054,33 @@ export function createGraphStore(db, adapter) {
|
|
|
934
1054
|
allowedGroups: result.allowed_groups
|
|
935
1055
|
};
|
|
936
1056
|
},
|
|
1057
|
+
async listUserModelAccessDefaults (ownerDID) {
|
|
1058
|
+
const results = await db.selectFrom('kubun_graph_user_model_access_defaults').select([
|
|
1059
|
+
'model_id',
|
|
1060
|
+
'permission_type',
|
|
1061
|
+
'access_level',
|
|
1062
|
+
'allowed_dids',
|
|
1063
|
+
'allowed_circles',
|
|
1064
|
+
'allowed_groups',
|
|
1065
|
+
'hlc',
|
|
1066
|
+
'removed'
|
|
1067
|
+
]).where('owner_did', '=', ownerDID).execute();
|
|
1068
|
+
return results.filter((result)=>!result.removed).map((result)=>({
|
|
1069
|
+
modelID: result.model_id,
|
|
1070
|
+
permissionType: result.permission_type,
|
|
1071
|
+
level: result.access_level,
|
|
1072
|
+
allowedDIDs: result.allowed_dids,
|
|
1073
|
+
allowedCircles: result.allowed_circles,
|
|
1074
|
+
allowedGroups: result.allowed_groups,
|
|
1075
|
+
hlc: result.hlc ?? null
|
|
1076
|
+
}));
|
|
1077
|
+
},
|
|
1078
|
+
async getUserModelAccessDefaultHLC (ownerDID, modelID, permissionType) {
|
|
1079
|
+
const result = await db.selectFrom('kubun_graph_user_model_access_defaults').select([
|
|
1080
|
+
'hlc'
|
|
1081
|
+
]).where('owner_did', '=', ownerDID).where('model_id', '=', modelID).where('permission_type', '=', permissionType).executeTakeFirst();
|
|
1082
|
+
return result?.hlc ?? null;
|
|
1083
|
+
},
|
|
937
1084
|
async setUserModelAccessDefault (params) {
|
|
938
1085
|
await db.insertInto('kubun_graph_user_model_access_defaults').values({
|
|
939
1086
|
owner_did: params.ownerDID,
|
|
@@ -942,33 +1089,102 @@ export function createGraphStore(db, adapter) {
|
|
|
942
1089
|
access_level: params.accessLevel,
|
|
943
1090
|
allowed_dids: params.allowedDIDs ? adapter.encodeJSON(params.allowedDIDs) : null,
|
|
944
1091
|
allowed_circles: params.allowedCircles ? adapter.encodeJSON(params.allowedCircles) : null,
|
|
945
|
-
allowed_groups: params.allowedGroups ? adapter.encodeJSON(params.allowedGroups) : null
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1092
|
+
allowed_groups: params.allowedGroups ? adapter.encodeJSON(params.allowedGroups) : null,
|
|
1093
|
+
hlc: params.hlc ?? null,
|
|
1094
|
+
// A set clears any prior tombstone so a re-established rule reads again.
|
|
1095
|
+
removed: 0
|
|
1096
|
+
}).onConflict((oc)=>{
|
|
1097
|
+
const updateValues = {
|
|
951
1098
|
access_level: params.accessLevel,
|
|
952
1099
|
allowed_dids: params.allowedDIDs ? adapter.encodeJSON(params.allowedDIDs) : null,
|
|
953
1100
|
allowed_circles: params.allowedCircles ? adapter.encodeJSON(params.allowedCircles) : null,
|
|
954
1101
|
allowed_groups: params.allowedGroups ? adapter.encodeJSON(params.allowedGroups) : null,
|
|
1102
|
+
removed: 0,
|
|
955
1103
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
956
|
-
}
|
|
1104
|
+
};
|
|
1105
|
+
if (params.hlc != null) {
|
|
1106
|
+
updateValues.hlc = params.hlc;
|
|
1107
|
+
}
|
|
1108
|
+
return oc.columns([
|
|
1109
|
+
'owner_did',
|
|
1110
|
+
'model_id',
|
|
1111
|
+
'permission_type'
|
|
1112
|
+
]).doUpdateSet(updateValues);
|
|
1113
|
+
}).execute();
|
|
957
1114
|
},
|
|
958
|
-
async removeUserModelAccessDefaults (ownerDID, modelID, permissionTypes) {
|
|
959
|
-
|
|
1115
|
+
async removeUserModelAccessDefaults (ownerDID, modelID, permissionTypes, hlc) {
|
|
1116
|
+
// Tombstone rather than delete: retain the row with rule fields nulled,
|
|
1117
|
+
// `removed` set, and `hlc` stamped so it stays the LWW anchor. Deleting
|
|
1118
|
+
// would erase the anchor and let a stale set revive the rule.
|
|
1119
|
+
//
|
|
1120
|
+
// Upsert rather than update-only, for the same reason. Broadcast delivery
|
|
1121
|
+
// is unordered, so a removal can arrive on a device that never stored the
|
|
1122
|
+
// rule it removes. An update would match no row, land no anchor, and the
|
|
1123
|
+
// older `set` arriving afterwards would then apply against a null anchor —
|
|
1124
|
+
// resurrecting a revoked grant. Inserting the tombstone makes the anchor
|
|
1125
|
+
// land whether or not the rule was ever seen.
|
|
1126
|
+
//
|
|
1127
|
+
// `access_level` is a non-null column that a tombstone has no meaningful
|
|
1128
|
+
// value for; a fresh tombstone stores the most restrictive level, and an
|
|
1129
|
+
// existing row keeps whatever it had. Either way the `removed` marker
|
|
1130
|
+
// hides it from reads.
|
|
1131
|
+
if (permissionTypes.length === 0) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const updatedAt = adapter.encodeTimestamp(new Date());
|
|
1135
|
+
await db.insertInto('kubun_graph_user_model_access_defaults').values(permissionTypes.map((permissionType)=>({
|
|
1136
|
+
owner_did: ownerDID,
|
|
1137
|
+
model_id: modelID,
|
|
1138
|
+
permission_type: permissionType,
|
|
1139
|
+
access_level: 'only_owner',
|
|
1140
|
+
allowed_dids: null,
|
|
1141
|
+
allowed_circles: null,
|
|
1142
|
+
allowed_groups: null,
|
|
1143
|
+
hlc,
|
|
1144
|
+
removed: 1
|
|
1145
|
+
}))).onConflict((oc)=>oc.columns([
|
|
1146
|
+
'owner_did',
|
|
1147
|
+
'model_id',
|
|
1148
|
+
'permission_type'
|
|
1149
|
+
]).doUpdateSet({
|
|
1150
|
+
allowed_dids: null,
|
|
1151
|
+
allowed_circles: null,
|
|
1152
|
+
allowed_groups: null,
|
|
1153
|
+
hlc,
|
|
1154
|
+
removed: 1,
|
|
1155
|
+
updated_at: updatedAt
|
|
1156
|
+
})).execute();
|
|
960
1157
|
},
|
|
961
1158
|
// --- Document scope / metadata helpers ---
|
|
962
1159
|
async getDocumentIDsForScope (scopes, excludedDocumentIDs = []) {
|
|
963
|
-
|
|
1160
|
+
// Group by model so each model's table is hit once with every owner it was
|
|
1161
|
+
// asked for, rather than once per (model, owner) pair.
|
|
1162
|
+
const ownersByModel = new Map();
|
|
964
1163
|
for (const scope of scopes){
|
|
1164
|
+
const owners = ownersByModel.get(scope.modelID);
|
|
1165
|
+
if (owners == null) {
|
|
1166
|
+
ownersByModel.set(scope.modelID, new Set([
|
|
1167
|
+
scope.ownerDID
|
|
1168
|
+
]));
|
|
1169
|
+
} else {
|
|
1170
|
+
owners.add(scope.ownerDID);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// Disjoint tables, so the reads go out together. Flattened in
|
|
1174
|
+
// `ownersByModel` order rather than completion order — the returned array
|
|
1175
|
+
// feeds merkle-tree construction, which must not vary run to run.
|
|
1176
|
+
const idsByModel = await Promise.all(Array.from(ownersByModel, async ([modelID, owners])=>{
|
|
965
1177
|
try {
|
|
966
|
-
const docs = await db.selectFrom(`k_${
|
|
967
|
-
|
|
1178
|
+
const docs = await db.selectFrom(`k_${modelID}`).select('id').where('owner', 'in', Array.from(owners)).execute();
|
|
1179
|
+
return docs.map((d)=>d.id);
|
|
968
1180
|
} catch (err) {
|
|
969
|
-
|
|
1181
|
+
// A model with no table contributes nothing — one missing table
|
|
1182
|
+
// skips that model's whole owner set at once.
|
|
1183
|
+
if (!isMissingTableError(err, `k_${modelID}`)) throw err;
|
|
1184
|
+
return [];
|
|
970
1185
|
}
|
|
971
|
-
}
|
|
1186
|
+
}));
|
|
1187
|
+
const docIDs = idsByModel.flat();
|
|
972
1188
|
if (excludedDocumentIDs.length > 0) {
|
|
973
1189
|
const excluded = new Set(excludedDocumentIDs);
|
|
974
1190
|
return docIDs.filter((id)=>!excluded.has(id));
|
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,5 +1,6 @@
|
|
|
1
1
|
export { createGraphStore } from './api.js';
|
|
2
2
|
export { graphStoreDefinition } from './definition.js';
|
|
3
|
+
export { ModelNotDeployedError } from './errors.js';
|
|
3
4
|
export const GRAPH_STORE = 'graph';
|
|
4
5
|
export function getGraphStore(provider) {
|
|
5
6
|
return provider.getStore(GRAPH_STORE);
|
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,4 +1,16 @@
|
|
|
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) {
|
|
2
14
|
const t = ctx.types;
|
|
3
15
|
const now = ctx.functions.now;
|
|
4
16
|
const init = {
|
|
@@ -20,7 +32,10 @@ export function getGraphMigrations(ctx) {
|
|
|
20
32
|
'document_model_id'
|
|
21
33
|
]).execute();
|
|
22
34
|
// 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)
|
|
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', [
|
|
24
39
|
'owner_did',
|
|
25
40
|
'model_id',
|
|
26
41
|
'permission_type'
|
|
@@ -40,8 +55,31 @@ export function getGraphMigrations(ctx) {
|
|
|
40
55
|
// Cluster definitions
|
|
41
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();
|
|
42
57
|
// 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())
|
|
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();
|
|
44
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
|
+
}
|
|
45
83
|
},
|
|
46
84
|
async down (db) {
|
|
47
85
|
await db.schema.dropTable('kubun_graph_catalogs').ifExists().execute();
|
package/lib/tables.d.ts
CHANGED
|
@@ -99,6 +99,10 @@ export type UserModelAccessDefaultTable = {
|
|
|
99
99
|
allowed_dids: JSONValueColumn<Array<string>> | null;
|
|
100
100
|
allowed_circles: JSONValueColumn<Array<string>> | null;
|
|
101
101
|
allowed_groups: JSONValueColumn<Array<string>> | null;
|
|
102
|
+
/** LWW anchor for replicated rules. Null for rows never stamped by a broadcast. */
|
|
103
|
+
hlc: string | null;
|
|
104
|
+
/** Retained-tombstone marker: 1 = removed, 0/null = active. */
|
|
105
|
+
removed: number | null;
|
|
102
106
|
created_at: CreatedAtColumn;
|
|
103
107
|
updated_at: UpdatedAtColumn;
|
|
104
108
|
};
|
|
@@ -112,6 +116,30 @@ export type CatalogTable = {
|
|
|
112
116
|
description: string;
|
|
113
117
|
filter_criteria: JSONValueColumn<CatalogFilterCriteria>;
|
|
114
118
|
hlc: string;
|
|
119
|
+
/**
|
|
120
|
+
* Local-only sync activation: 1 = active (scopes sync), 0/null = known (a
|
|
121
|
+
* discovered catalog that has not been activated). Insert-optional; own
|
|
122
|
+
* catalogs are created active, discovered ones arrive known.
|
|
123
|
+
*/
|
|
124
|
+
active: ColumnType<number | null, number | null | undefined, number | null>;
|
|
125
|
+
/**
|
|
126
|
+
* Provenance: the group this catalog was first discovered from, null for own
|
|
127
|
+
* catalogs. First-discovery only — never overwritten on re-discovery.
|
|
128
|
+
*/
|
|
129
|
+
source_group_id: ColumnType<string | null, string | null | undefined, string | null>;
|
|
130
|
+
/**
|
|
131
|
+
* Provenance: the circle whose `catalog_ids` first referenced this catalog
|
|
132
|
+
* (set only for invite-seeded discovery), null otherwise. First-discovery only.
|
|
133
|
+
*/
|
|
134
|
+
source_circle_id: ColumnType<string | null, string | null | undefined, string | null>;
|
|
135
|
+
/**
|
|
136
|
+
* The creator-signed `catalog:set` token that authenticated this catalog: own
|
|
137
|
+
* catalogs sign it at creation, discovered ones store the verified token that
|
|
138
|
+
* delivered them. Forwarded verbatim in invite seeds so a joiner re-verifies
|
|
139
|
+
* creator-binding (`ownerDID === iss`) with the same helper the broadcast
|
|
140
|
+
* receive path uses. Null for rows that predate token capture.
|
|
141
|
+
*/
|
|
142
|
+
signed_token: ColumnType<string | null, string | null | undefined, string | null>;
|
|
115
143
|
created_at: CreatedAtColumn;
|
|
116
144
|
updated_at: UpdatedAtColumn;
|
|
117
145
|
};
|
|
@@ -142,6 +170,13 @@ export type CreateDocumentParams = DocumentParams & {
|
|
|
142
170
|
export type SaveDocumentParams = DocumentParams & {
|
|
143
171
|
existing: DocumentNode;
|
|
144
172
|
};
|
|
173
|
+
/** One cluster's registration, written with the graph that deploys it. */
|
|
174
|
+
export type CreateGraphCluster = {
|
|
175
|
+
/** The cluster definition, stored verbatim for a peer to ship onward. */
|
|
176
|
+
definition: unknown;
|
|
177
|
+
/** modelID → index within the cluster. */
|
|
178
|
+
models: Record<string, number>;
|
|
179
|
+
};
|
|
145
180
|
export type CreateGraphParams = {
|
|
146
181
|
aliases?: Record<string, string>;
|
|
147
182
|
extensionSDL?: string;
|
|
@@ -150,6 +185,13 @@ export type CreateGraphParams = {
|
|
|
150
185
|
pluginConfig?: Record<string, Record<string, unknown>>;
|
|
151
186
|
record: DocumentModelsRecord;
|
|
152
187
|
search?: SearchConfig;
|
|
188
|
+
/**
|
|
189
|
+
* Clusters to register in the SAME transaction as the graph. The cluster a
|
|
190
|
+
* model was deployed in is what lets a peer ship that model's definition to a
|
|
191
|
+
* device that lacks it, so a graph written without its clusters is one that
|
|
192
|
+
* works locally and cannot be synced to a device that has never seen it.
|
|
193
|
+
*/
|
|
194
|
+
clusters?: Record<string, CreateGraphCluster>;
|
|
153
195
|
};
|
|
154
196
|
export type AddDocumentModelParams = {
|
|
155
197
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/store-graph",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "see LICENSE.md",
|
|
3
|
+
"version": "0.12.0",
|
|
5
4
|
"keywords": [],
|
|
5
|
+
"license": "see LICENSE.md",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"type": "module",
|
|
7
|
-
"main": "lib/index.js",
|
|
8
|
-
"types": "lib/index.d.ts",
|
|
9
8
|
"exports": {
|
|
10
9
|
".": "./lib/index.js"
|
|
11
10
|
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"types": "lib/index.d.ts",
|
|
12
13
|
"files": [
|
|
13
14
|
"lib/*",
|
|
14
15
|
"LICENSE.md"
|
|
15
16
|
],
|
|
16
|
-
"sideEffects": false,
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@sozai/codec": "^0.
|
|
19
|
-
"kysely": "^0.29.
|
|
20
|
-
"@kubun/db": "^0.
|
|
21
|
-
"@kubun/db-adapter": "^0.
|
|
22
|
-
"@kubun/
|
|
23
|
-
"@kubun/
|
|
18
|
+
"@sozai/codec": "^0.4.0",
|
|
19
|
+
"kysely": "^0.29.4",
|
|
20
|
+
"@kubun/db": "^0.12.0",
|
|
21
|
+
"@kubun/db-adapter": "^0.12.0",
|
|
22
|
+
"@kubun/protocol": "^0.12.0",
|
|
23
|
+
"@kubun/id": "^0.12.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@testcontainers/postgresql": "^12.0
|
|
27
|
-
"@kubun/db-better-sqlite": "^0.
|
|
28
|
-
"@kubun/db-postgres": "^0.
|
|
26
|
+
"@testcontainers/postgresql": "^12.1.0",
|
|
27
|
+
"@kubun/db-better-sqlite": "^0.12.0",
|
|
28
|
+
"@kubun/db-postgres": "^0.12.0"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
+
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
31
32
|
"build:clean": "del lib",
|
|
32
33
|
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
33
34
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
34
35
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
35
|
-
"
|
|
36
|
+
"test": "pnpm run test:types && pnpm run test:unit",
|
|
36
37
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
37
|
-
"test:unit": "vitest run"
|
|
38
|
-
"test": "pnpm run test:types && pnpm run test:unit"
|
|
38
|
+
"test:unit": "vitest run"
|
|
39
39
|
}
|
|
40
40
|
}
|