@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.
@@ -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;
@@ -98,6 +99,10 @@ export type UserModelAccessDefaultTable = {
98
99
  allowed_dids: JSONValueColumn<Array<string>> | null;
99
100
  allowed_circles: JSONValueColumn<Array<string>> | null;
100
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;
101
106
  created_at: CreatedAtColumn;
102
107
  updated_at: UpdatedAtColumn;
103
108
  };
@@ -111,6 +116,30 @@ export type CatalogTable = {
111
116
  description: string;
112
117
  filter_criteria: JSONValueColumn<CatalogFilterCriteria>;
113
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>;
114
143
  created_at: CreatedAtColumn;
115
144
  updated_at: UpdatedAtColumn;
116
145
  };
@@ -141,6 +170,13 @@ export type CreateDocumentParams = DocumentParams & {
141
170
  export type SaveDocumentParams = DocumentParams & {
142
171
  existing: DocumentNode;
143
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
+ };
144
180
  export type CreateGraphParams = {
145
181
  aliases?: Record<string, string>;
146
182
  extensionSDL?: string;
@@ -149,6 +185,13 @@ export type CreateGraphParams = {
149
185
  pluginConfig?: Record<string, Record<string, unknown>>;
150
186
  record: DocumentModelsRecord;
151
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>;
152
195
  };
153
196
  export type AddDocumentModelParams = {
154
197
  id: string;
package/lib/tables.js CHANGED
@@ -1 +1 @@
1
- export{};
1
+ export { };
package/package.json CHANGED
@@ -1,40 +1,40 @@
1
1
  {
2
2
  "name": "@kubun/store-graph",
3
- "version": "0.10.2",
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
- "@enkaku/codec": "^0.16.0",
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"
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.1",
27
- "@kubun/db-better-sqlite": "^0.10.0",
28
- "@kubun/db-postgres": "^0.10.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
- "build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
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
- "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
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
  }