@kubun/store-graph 0.17.0 → 0.18.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/migrations.js CHANGED
@@ -1,22 +1,21 @@
1
+ import { sql } from 'kysely';
1
2
  /**
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.
3
+ * A shipped migration should be immutable: once a database has run it, editing
4
+ * it changes nothing on that database and silently diverges its schema from the
5
+ * code. Schema changes after a release that preserves databases need a NEW
6
+ * migration.
6
7
  *
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.
8
+ * `0-init` was edited in place again for the pending arrival group, sender, retry columns and
9
+ * due index. Existing databases will not acquire them and must be recreated.
10
+ * This is a breaking reset. Do NOT repeat an in-place edit once a release that
11
+ * must preserve existing databases ships; add a new migration then.
13
12
  */ export function getGraphMigrations(ctx) {
14
13
  const t = ctx.types;
15
14
  const now = ctx.functions.now;
16
15
  const init = {
17
16
  async up (db) {
18
17
  // Document models
19
- await db.schema.createTable('kubun_graph_document_models').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('version', t.text, (col)=>col.notNull()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('behavior', t.text, (col)=>col.notNull()).addColumn('unique_fields', t.json).addColumn('interfaces', t.json, (col)=>col.notNull()).addColumn('schema', t.json, (col)=>col.notNull()).addColumn('fields_meta', t.json, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
18
+ await db.schema.createTable('kubun_graph_document_models').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('version', t.text, (col)=>col.notNull()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('behavior', t.text, (col)=>col.notNull()).addColumn('unique_fields', t.json).addColumn('interfaces', t.json, (col)=>col.notNull()).addColumn('schema', t.json, (col)=>col.notNull()).addColumn('fields_meta', t.json, (col)=>col.notNull()).addColumn('provider_merge_policy', t.json).addColumn('date_field', t.text).addColumn('resolved_date_field', t.text).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
20
19
  // Document model interfaces
21
20
  await db.schema.createTable('kubun_graph_document_model_interfaces').ifNotExists().addColumn('interface_id', t.text, (col)=>col.notNull().references('kubun_graph_document_models.id')).addColumn('implementation_id', t.text, (col)=>col.notNull().references('kubun_graph_document_models.id')).addUniqueConstraint('kubun_graph_document_model_interfaces_pkey', [
22
21
  'interface_id',
@@ -47,22 +46,97 @@
47
46
  'owner_did',
48
47
  'model_id'
49
48
  ]).execute();
49
+ // The group row serialises concurrent parking on PostgreSQL. SQLite serialises writers.
50
+ await db.schema.createTable('kubun_graph_pending_groups').ifNotExists().addColumn('group_id', t.text, (col)=>col.primaryKey()).execute();
50
51
  // Mutation log
51
- 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();
52
+ 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('arrival_group_id', t.text).addColumn('sender_peer_did', t.text).addColumn('parked_at', t.bigint).addColumn('status', t.text, (col)=>col.notNull().defaultTo('applied')).addColumn('attempt_count', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('last_attempt_at', t.bigint).addColumn('next_attempt_at', t.bigint).execute();
52
53
  await db.schema.createIndex('idx_graph_mutation_log_hlc').ifNotExists().on('kubun_graph_mutation_log').column('hlc').execute();
53
54
  await db.schema.createIndex('idx_graph_mutation_log_document').ifNotExists().on('kubun_graph_mutation_log').column('document_id').execute();
54
55
  await db.schema.createIndex('idx_graph_mutation_log_status').ifNotExists().on('kubun_graph_mutation_log').column('status').execute();
56
+ await db.schema.createIndex('idx_graph_mutation_log_pending_group').ifNotExists().on('kubun_graph_mutation_log').columns([
57
+ 'status',
58
+ 'arrival_group_id',
59
+ 'sender_peer_did'
60
+ ]).execute();
61
+ await db.schema.createIndex('idx_graph_mutation_log_due_pending').ifNotExists().on('kubun_graph_mutation_log').columns([
62
+ 'status',
63
+ 'next_attempt_at'
64
+ ]).execute();
65
+ await db.schema.createTable('kubun_graph_catalog_use').ifNotExists().addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('catalog_id', t.text, (col)=>col.notNull()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('rules', t.json, (col)=>col.notNull()).addColumn('level', t.text).addColumn('followed', 'integer', (col)=>col.notNull().defaultTo(0)).addPrimaryKeyConstraint('kubun_graph_catalog_use_pkey', [
66
+ 'owner_did',
67
+ 'catalog_id'
68
+ ]).execute();
69
+ await db.schema.createTable('kubun_graph_scope_identity').ifNotExists().addColumn('id', 'integer', (col)=>col.primaryKey()).addColumn('reader_did', t.text, (col)=>col.notNull()).addColumn('default_read_level', t.text, (col)=>col.notNull()).execute();
70
+ await db.schema.createTable('kubun_graph_catalog_scope_tick').ifNotExists().addColumn('id', 'integer', (col)=>col.primaryKey()).addColumn('last_at', t.bigint, (col)=>col.notNull()).execute();
71
+ await db.schema.createTable('kubun_graph_scope_changes').ifNotExists().addColumn('document_id', t.text, (col)=>col.primaryKey()).addColumn('version', 'integer', (col)=>col.notNull()).execute();
72
+ await db.schema.createTable('kubun_graph_document_refs').ifNotExists().addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('ref', t.text, (col)=>col.notNull()).addColumn('displays', 'integer', (col)=>col.notNull()).addColumn('retains', 'integer', (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_graph_document_refs_pkey', [
73
+ 'document_id',
74
+ 'ref'
75
+ ]).execute();
76
+ await db.schema.createTable('kubun_graph_gc_marks').ifNotExists().addColumn('document_id', t.text, (col)=>col.primaryKey()).addColumn('digest', t.text, (col)=>col.notNull()).addColumn('marked_at', t.bigint, (col)=>col.notNull()).execute();
77
+ await db.schema.createTable('kubun_graph_custody_scans').ifNotExists().addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('catalog_id', t.text, (col)=>col.notNull()).addColumn('after_document_id', t.text).addPrimaryKeyConstraint('kubun_graph_custody_scans_pkey', [
78
+ 'owner_did',
79
+ 'catalog_id'
80
+ ]).execute();
81
+ await db.schema.createTable('kubun_graph_custody_nonces').ifNotExists().addColumn('nonce', t.text, (col)=>col.primaryKey()).addColumn('custodian', t.text, (col)=>col.notNull()).addColumn('assignment_digest', t.text, (col)=>col.notNull()).addColumn('expires_at', t.bigint, (col)=>col.notNull()).execute();
82
+ await db.schema.createTable('kubun_graph_custody_receipts').ifNotExists().addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('custodian', t.text, (col)=>col.notNull()).addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('custody_owner', t.text, (col)=>col.notNull()).addColumn('catalog_owner', t.text, (col)=>col.notNull()).addColumn('catalog_id', t.text, (col)=>col.notNull()).addColumn('assignment_digest', t.text, (col)=>col.notNull()).addColumn('head_digest', t.text, (col)=>col.notNull()).addColumn('mutation_digest', t.text, (col)=>col.notNull()).addColumn('received_at', t.bigint, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_graph_custody_receipts_pkey', [
83
+ 'document_id',
84
+ 'custodian'
85
+ ]).execute();
86
+ await db.schema.createTable('kubun_graph_document_holds').ifNotExists().addColumn('hold_id', t.text, (col)=>col.primaryKey()).addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('created_by', t.text, (col)=>col.notNull()).addColumn('reason', t.text).addColumn('expires_at', t.bigint).addColumn('released_at', t.bigint).execute();
87
+ await db.schema.createIndex('idx_graph_document_holds_document').ifNotExists().on('kubun_graph_document_holds').column('document_id').execute();
88
+ // Aging finds the holds that expired since its last tick.
89
+ await db.schema.createIndex('idx_graph_document_holds_expires_at').ifNotExists().on('kubun_graph_document_holds').column('expires_at').execute();
90
+ await db.schema.createTable('kubun_graph_document_scope_dates').ifNotExists().addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('field', t.text, (col)=>col.notNull()).addColumn('scope_date', t.bigint, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_graph_document_scope_dates_pkey', [
91
+ 'document_id',
92
+ 'field'
93
+ ]).execute();
94
+ // Documents this device swept: what a ref recompute reads of each, so a rule matching it
95
+ // again can be told apart from a document never received. Local only.
96
+ await db.schema.createTable('kubun_graph_swept_documents').ifNotExists().addColumn('document_id', t.text, (col)=>col.primaryKey()).addColumn('model_id', t.text, (col)=>col.notNull()).addColumn('owner', t.text, (col)=>col.notNull()).addColumn('created_at', t.bigint, (col)=>col.notNull()).addColumn('scope_date', t.bigint).addColumn('field_dates', t.text, (col)=>col.notNull()).addColumn('read_access', t.text).addColumn('mutation_digest', t.text, (col)=>col.notNull()).addColumn('swept_at', t.bigint, (col)=>col.notNull()).addColumn('in_scope', 'integer', (col)=>col.notNull()).addColumn('scope_left_at', t.bigint, (col)=>col.notNull()).execute();
97
+ await db.schema.createIndex('idx_graph_swept_documents_model').ifNotExists().on('kubun_graph_swept_documents').column('model_id').execute();
98
+ await db.schema.createIndex('idx_graph_document_scope_dates_window').ifNotExists().on('kubun_graph_document_scope_dates').columns([
99
+ 'field',
100
+ 'scope_date'
101
+ ]).execute();
102
+ // Catalog holder lookups are bytewise prefix ranges. On Postgres they
103
+ // compare `ref collate "C"`, which an index under a linguistic database
104
+ // collation cannot serve; SQLite compares bytewise already.
105
+ const refIndex = db.schema.createIndex('idx_graph_document_refs_ref').ifNotExists().on('kubun_graph_document_refs');
106
+ await (ctx.dialect === 'postgres' ? refIndex.expression(sql`ref collate "C"`) : refIndex.column('ref')).execute();
55
107
  // Cluster models (model -> cluster mapping)
56
108
  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();
57
109
  await db.schema.createIndex('idx_graph_cluster_models_cluster').ifNotExists().on('kubun_graph_cluster_models').column('cluster_id').execute();
58
110
  // Cluster definitions
59
111
  await db.schema.createTable('kubun_graph_clusters').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('definition', t.json, (col)=>col.notNull()).execute();
112
+ await db.schema.createTable('kubun_graph_access_default_records').ifNotExists().addColumn('type', t.text, (col)=>col.notNull()).addColumn('owner', t.text, (col)=>col.notNull()).addColumn('id', t.text, (col)=>col.notNull()).addColumn('op_hash', t.text, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('op_jwt', t.text, (col)=>col.notNull()).addColumn('payload', t.binary).addPrimaryKeyConstraint('kubun_graph_access_default_records_pkey', [
113
+ 'type',
114
+ 'owner',
115
+ 'id'
116
+ ]).execute();
117
+ await db.schema.createTable('kubun_graph_catalog_drafts').ifNotExists().addColumn('catalog_id', t.text, (col)=>col.primaryKey()).addColumn('state', t.text, (col)=>col.notNull()).execute();
118
+ await db.schema.createTable('kubun_graph_catalog_publications').ifNotExists().addColumn('catalog_id', t.text, (col)=>col.notNull()).addColumn('version_id', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_graph_catalog_publications_pkey', [
119
+ 'catalog_id',
120
+ 'version_id'
121
+ ]).execute();
122
+ await db.schema.createTable('kubun_graph_catalog_recipient_versions').ifNotExists().addColumn('catalog_id', t.text, (col)=>col.notNull()).addColumn('version_id', t.text, (col)=>col.notNull()).addColumn('signed_record', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_graph_catalog_recipient_versions_pkey', [
123
+ 'catalog_id',
124
+ 'version_id'
125
+ ]).execute();
126
+ await db.schema.createTable('kubun_graph_catalog_pending_heads').ifNotExists().addColumn('catalog_id', t.text, (col)=>col.primaryKey()).addColumn('head_version_id', t.text, (col)=>col.notNull()).addColumn('group_id', t.text).addColumn('retry_at', t.bigint, (col)=>col.notNull()).addColumn('attempts', 'integer', (col)=>col.notNull()).execute();
127
+ await db.schema.createTable('kubun_graph_catalog_admitted_heads').ifNotExists().addColumn('catalog_id', t.text, (col)=>col.primaryKey()).addColumn('signed_record', t.text, (col)=>col.notNull()).execute();
60
128
  // Catalogs
61
- 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).
62
- .addColumn('active', 'integer')// First-discovery provenance (nullable; never overwritten on re-discovery).
63
- .addColumn('source_group_id', t.text).addColumn('source_circle_id', t.text)// Creator-signed `catalog:set` token; forwarded verbatim in invite seeds.
64
- .addColumn('signed_token', t.text).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
65
- await db.schema.createIndex('idx_graph_catalogs_owner').ifNotExists().on('kubun_graph_catalogs').column('owner_did').execute();
129
+ await db.schema.createTable('kubun_graph_catalog_records').ifNotExists().addColumn('type', t.text, (col)=>col.notNull()).addColumn('owner', t.text, (col)=>col.notNull()).addColumn('id', t.text, (col)=>col.notNull()).addColumn('op_hash', t.text, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('op_jwt', t.text, (col)=>col.notNull()).addColumn('payload', t.binary).addColumn('catalog_id', t.text).addColumn('group_id', t.text).addColumn('circle_id', t.text).addPrimaryKeyConstraint('kubun_graph_catalog_records_pkey', [
130
+ 'type',
131
+ 'owner',
132
+ 'id',
133
+ 'op_hash'
134
+ ]).execute();
135
+ await db.schema.createIndex('idx_graph_catalog_record_group').ifNotExists().on('kubun_graph_catalog_records').columns([
136
+ 'group_id',
137
+ 'catalog_id',
138
+ 'circle_id'
139
+ ]).execute();
66
140
  // `.ifNotExists()` protects a half-created table from being recreated —
67
141
  // and, because this migration was edited in place, it equally protects a
68
142
  // table an EARLIER run created from ever acquiring the columns that edit
@@ -85,11 +159,30 @@
85
159
  }
86
160
  },
87
161
  async down (db) {
162
+ await db.schema.dropTable('kubun_graph_catalog_drafts').ifExists().execute();
163
+ await db.schema.dropTable('kubun_graph_catalog_publications').ifExists().execute();
164
+ await db.schema.dropTable('kubun_graph_catalog_recipient_versions').ifExists().execute();
165
+ await db.schema.dropTable('kubun_graph_catalog_pending_heads').ifExists().execute();
166
+ await db.schema.dropTable('kubun_graph_catalog_admitted_heads').ifExists().execute();
167
+ await db.schema.dropTable('kubun_graph_access_default_records').ifExists().execute();
168
+ await db.schema.dropTable('kubun_graph_catalog_records').ifExists().execute();
169
+ await db.schema.dropTable('kubun_graph_scope_changes').ifExists().execute();
170
+ await db.schema.dropTable('kubun_graph_catalog_scope_tick').ifExists().execute();
171
+ await db.schema.dropTable('kubun_graph_scope_identity').ifExists().execute();
172
+ await db.schema.dropTable('kubun_graph_document_scope_dates').ifExists().execute();
173
+ await db.schema.dropTable('kubun_graph_swept_documents').ifExists().execute();
174
+ await db.schema.dropTable('kubun_graph_gc_marks').ifExists().execute();
175
+ await db.schema.dropTable('kubun_graph_custody_scans').ifExists().execute();
176
+ await db.schema.dropTable('kubun_graph_custody_nonces').ifExists().execute();
177
+ await db.schema.dropTable('kubun_graph_custody_receipts').ifExists().execute();
178
+ await db.schema.dropTable('kubun_graph_document_holds').ifExists().execute();
179
+ await db.schema.dropTable('kubun_graph_document_refs').ifExists().execute();
180
+ await db.schema.dropTable('kubun_graph_catalog_use').ifExists().execute();
88
181
  await db.schema.dropTable('kubun_graph_search_models').ifExists().execute();
89
- await db.schema.dropTable('kubun_graph_catalogs').ifExists().execute();
90
182
  await db.schema.dropTable('kubun_graph_clusters').ifExists().execute();
91
183
  await db.schema.dropTable('kubun_graph_cluster_models').ifExists().execute();
92
184
  await db.schema.dropTable('kubun_graph_mutation_log').ifExists().execute();
185
+ await db.schema.dropTable('kubun_graph_pending_groups').ifExists().execute();
93
186
  await db.schema.dropTable('kubun_graph_user_model_access_defaults').ifExists().execute();
94
187
  await db.schema.dropTable('kubun_graph_document_model_links').ifExists().execute();
95
188
  await db.schema.dropTable('kubun_graph_models').ifExists().execute();
@@ -315,7 +315,7 @@ export function applyValueFilter(eb, keys, filter, adapter, coerce) {
315
315
  const items = [
316
316
  ...new Set(value)
317
317
  ];
318
- // Vacuously true (D7): every array includes the empty set. Handled here, not in
318
+ // Vacuously true: every array includes the empty set. Handled here, not in
319
319
  // SQL, so the adapter seam never sees an empty IN-list.
320
320
  return items.length === 0 ? sql`1 = 1` : adapter.arrayIncludesAllPredicate(getRawKeyPath(eb, keys), items);
321
321
  }
package/lib/tables.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { CreatedAtColumn, UpdatedAtColumn } from '@kubun/db-adapter';
2
2
  import type { DocumentID } from '@kubun/id';
3
- import type { CatalogFilterCriteria, DocumentFieldsMeta, DocumentFilter, DocumentModelSchema, DocumentModelsRecord, DocumentNode, DocumentOrderBy } from '@kubun/protocol';
4
- import type { ColumnType, Insertable, Selectable, Updateable } from 'kysely';
3
+ import type { AccessLevel, DocumentFieldsMeta, DocumentFilter, DocumentModel, DocumentModelSchema, DocumentModelsRecord, DocumentNode, DocumentOrderBy } from '@kubun/protocol';
4
+ import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';
5
5
  type JSONValueColumn<T> = ColumnType<T, T, T>;
6
6
  export type DocumentData = Record<string, unknown>;
7
7
  export type DocumentTable<Data extends DocumentData = DocumentData> = {
@@ -14,6 +14,9 @@ export type DocumentTable<Data extends DocumentData = DocumentData> = {
14
14
  unique: Uint8Array;
15
15
  created_at: CreatedAtColumn;
16
16
  updated_at: UpdatedAtColumn;
17
+ mutation_created_at: ColumnType<number | null, number | null | undefined, number | null>;
18
+ mutation_updated_at: ColumnType<number | null, number | null | undefined, number | null>;
19
+ scope_date: ColumnType<number | null, number | null | undefined, number | null>;
17
20
  };
18
21
  export type Document<Data extends DocumentData = DocumentData> = Selectable<DocumentTable<Data>>;
19
22
  export type InsertDocument<Data extends DocumentData = DocumentData> = Insertable<DocumentTable<Data>>;
@@ -27,6 +30,11 @@ export type DocumentModelTable = {
27
30
  interfaces: JSONValueColumn<Array<string>>;
28
31
  schema: JSONValueColumn<DocumentModelSchema>;
29
32
  fields_meta: JSONValueColumn<DocumentFieldsMeta>;
33
+ provider_merge_policy: JSONValueColumn<{
34
+ group: Array<string>;
35
+ }> | null;
36
+ date_field: ColumnType<string | null, string | null | undefined, string | null>;
37
+ resolved_date_field: ColumnType<string | null, string | null | undefined, string | null>;
30
38
  created_at: CreatedAtColumn;
31
39
  updated_at: UpdatedAtColumn;
32
40
  };
@@ -71,13 +79,139 @@ export type MutationLogTable = {
71
79
  mutation_hash: string;
72
80
  model_id: string;
73
81
  document_id: string;
82
+ /** Unverified signed issuer, informational only. Caps and authorisation use sender_peer_did. */
74
83
  author_did: string;
75
84
  hlc: string;
76
85
  mutation_jwt: string;
86
+ arrival_group_id: ColumnType<string | null, string | null | undefined, string | null>;
87
+ sender_peer_did: ColumnType<string | null, string | null | undefined, string | null>;
88
+ parked_at: ColumnType<number | null, number | null | undefined, number | null>;
77
89
  status: string;
90
+ attempt_count: Generated<number>;
91
+ last_attempt_at: ColumnType<number | null, number | null | undefined, number | null>;
92
+ next_attempt_at: ColumnType<number | null, number | null | undefined, number | null>;
78
93
  };
79
94
  export type MutationLogEntry = Selectable<MutationLogTable>;
80
95
  export type InsertMutationLogEntry = Insertable<MutationLogTable>;
96
+ /**
97
+ * One concrete rule as a document-leaf page selects by it: a model, the owners
98
+ * (null: any owner), and an optional window `[from, to)` over a date field.
99
+ */
100
+ export type DocumentLeafRule = {
101
+ modelID: string;
102
+ owners: ReadonlyArray<string> | null;
103
+ dateField: string | null;
104
+ from: number | null;
105
+ to: number | null;
106
+ };
107
+ export type DocumentLeafPageParams = {
108
+ groupRead?: {
109
+ groupID: string;
110
+ circleIDs: Array<string>;
111
+ };
112
+ rules: ReadonlyArray<DocumentLeafRule>;
113
+ /** Exclusive lower document key; null starts at the first key. */
114
+ after: string | null;
115
+ /** Inclusive upper document key; null reads to the last key. */
116
+ through?: string | null;
117
+ limit: number;
118
+ };
119
+ /** A stored document matching a rule, with the IDs of its applied mutations, sorted. */
120
+ export type DocumentLeafRow = {
121
+ documentID: string;
122
+ owner: string;
123
+ /** The document was created on this device rather than received. */
124
+ authoredHere: boolean;
125
+ acceptedMutationIDs: Array<string>;
126
+ };
127
+ export type CatalogUseTable = {
128
+ owner_did: string;
129
+ catalog_id: string;
130
+ name: string;
131
+ rules: JSONValueColumn<Array<{
132
+ id: string;
133
+ models?: Array<string>;
134
+ owners?: Array<string>;
135
+ maxAge?: string;
136
+ maxAhead?: string;
137
+ dateField?: string;
138
+ }>>;
139
+ level: 'KNOWN' | 'OFFLINE' | 'SERVE_ONLY' | null;
140
+ followed: number;
141
+ };
142
+ export type DocumentRefTable = {
143
+ document_id: string;
144
+ ref: string;
145
+ displays: number;
146
+ retains: number;
147
+ };
148
+ export type CustodyScanTable = {
149
+ owner_did: string;
150
+ catalog_id: string;
151
+ after_document_id: string | null;
152
+ };
153
+ export type GCMarkTable = {
154
+ document_id: string;
155
+ digest: string;
156
+ marked_at: number;
157
+ };
158
+ export type CustodyNonceTable = {
159
+ nonce: string;
160
+ custodian: string;
161
+ assignment_digest: string;
162
+ expires_at: number;
163
+ };
164
+ export type CustodyReceiptTable = {
165
+ document_id: string;
166
+ custodian: string;
167
+ group_id: string;
168
+ custody_owner: string;
169
+ catalog_owner: string;
170
+ catalog_id: string;
171
+ assignment_digest: string;
172
+ head_digest: string;
173
+ mutation_digest: string;
174
+ received_at: number;
175
+ };
176
+ export type DocumentHoldTable = {
177
+ hold_id: string;
178
+ document_id: string;
179
+ created_by: string;
180
+ reason: string | null;
181
+ expires_at: number | null;
182
+ released_at: number | null;
183
+ };
184
+ export type DocumentScopeDateTable = {
185
+ document_id: string;
186
+ field: string;
187
+ scope_date: number;
188
+ };
189
+ export type SweptDocumentTable = {
190
+ document_id: string;
191
+ model_id: string;
192
+ owner: string;
193
+ created_at: number;
194
+ scope_date: number | null;
195
+ field_dates: string;
196
+ read_access: string | null;
197
+ mutation_digest: string;
198
+ swept_at: number;
199
+ in_scope: number;
200
+ scope_left_at: number;
201
+ };
202
+ export type ScopeIdentityTable = {
203
+ id: number;
204
+ reader_did: string;
205
+ default_read_level: AccessLevel;
206
+ };
207
+ export type CatalogScopeTickTable = {
208
+ id: number;
209
+ last_at: number;
210
+ };
211
+ export type CatalogScopeChangeTable = {
212
+ document_id: string;
213
+ version: number;
214
+ };
81
215
  export type ClusterModelTable = {
82
216
  model_id: string;
83
217
  cluster_id: string;
@@ -108,45 +242,58 @@ export type UserModelAccessDefaultTable = {
108
242
  export type UserModelAccessDefault = Selectable<UserModelAccessDefaultTable>;
109
243
  export type InsertUserModelAccessDefault = Insertable<UserModelAccessDefaultTable>;
110
244
  export type UpdateUserModelAccessDefault = Updateable<UserModelAccessDefaultTable>;
111
- export type CatalogTable = {
245
+ export type GraphCatalogRecordTable = {
246
+ type: string;
247
+ owner: string;
112
248
  id: string;
113
- owner_did: string;
114
- name: string;
115
- description: string;
116
- filter_criteria: JSONValueColumn<CatalogFilterCriteria>;
249
+ op_hash: string;
117
250
  hlc: string;
118
- /**
119
- * Local-only sync activation: 1 = active (scopes sync), 0/null = known (a
120
- * discovered catalog that has not been activated). Insert-optional; own
121
- * catalogs are created active, discovered ones arrive known.
122
- */
123
- active: ColumnType<number | null, number | null | undefined, number | null>;
124
- /**
125
- * Provenance: the group this catalog was first discovered from, null for own
126
- * catalogs. First-discovery only — never overwritten on re-discovery.
127
- */
128
- source_group_id: ColumnType<string | null, string | null | undefined, string | null>;
129
- /**
130
- * Provenance: the circle whose `catalog_ids` first referenced this catalog
131
- * (set only for invite-seeded discovery), null otherwise. First-discovery only.
132
- */
133
- source_circle_id: ColumnType<string | null, string | null | undefined, string | null>;
134
- /**
135
- * The creator-signed `catalog:set` token that authenticated this catalog: own
136
- * catalogs sign it at creation, discovered ones store the verified token that
137
- * delivered them. Forwarded verbatim in invite seeds so a joiner re-verifies
138
- * creator-binding (`ownerDID === iss`) with the same helper the broadcast
139
- * receive path uses. Null for rows that predate token capture.
140
- */
141
- signed_token: ColumnType<string | null, string | null | undefined, string | null>;
142
- created_at: CreatedAtColumn;
143
- updated_at: UpdatedAtColumn;
251
+ op_jwt: string;
252
+ payload: Uint8Array | null;
253
+ catalog_id: string | null;
254
+ group_id: string | null;
255
+ circle_id: string | null;
144
256
  };
145
- export type Catalog = Selectable<CatalogTable>;
146
- export type InsertCatalog = Insertable<CatalogTable>;
147
- export type UpdateCatalog = Updateable<CatalogTable>;
257
+ export type GraphCatalogRecordRow = Selectable<GraphCatalogRecordTable>;
148
258
  export type GraphTables = {
149
- kubun_graph_catalogs: CatalogTable;
259
+ kubun_graph_catalog_admitted_heads: {
260
+ catalog_id: string;
261
+ signed_record: string;
262
+ };
263
+ kubun_graph_catalog_drafts: {
264
+ catalog_id: string;
265
+ state: string;
266
+ };
267
+ kubun_graph_catalog_publications: {
268
+ catalog_id: string;
269
+ version_id: string;
270
+ };
271
+ kubun_graph_catalog_recipient_versions: {
272
+ catalog_id: string;
273
+ version_id: string;
274
+ signed_record: string;
275
+ };
276
+ kubun_graph_catalog_pending_heads: {
277
+ catalog_id: string;
278
+ head_version_id: string;
279
+ group_id: string | null;
280
+ retry_at: number;
281
+ attempts: number;
282
+ };
283
+ kubun_graph_access_default_records: Omit<GraphCatalogRecordTable, 'catalog_id' | 'group_id' | 'circle_id'>;
284
+ kubun_graph_catalog_records: GraphCatalogRecordTable;
285
+ kubun_graph_scope_changes: CatalogScopeChangeTable;
286
+ kubun_graph_catalog_scope_tick: CatalogScopeTickTable;
287
+ kubun_graph_scope_identity: ScopeIdentityTable;
288
+ kubun_graph_catalog_use: CatalogUseTable;
289
+ kubun_graph_document_refs: DocumentRefTable;
290
+ kubun_graph_document_holds: DocumentHoldTable;
291
+ kubun_graph_gc_marks: GCMarkTable;
292
+ kubun_graph_custody_scans: CustodyScanTable;
293
+ kubun_graph_custody_nonces: CustodyNonceTable;
294
+ kubun_graph_custody_receipts: CustodyReceiptTable;
295
+ kubun_graph_document_scope_dates: DocumentScopeDateTable;
296
+ kubun_graph_swept_documents: SweptDocumentTable;
150
297
  kubun_graph_cluster_models: ClusterModelTable;
151
298
  kubun_graph_clusters: ClusterDefinitionTable;
152
299
  kubun_graph_document_models: DocumentModelTable;
@@ -154,6 +301,9 @@ export type GraphTables = {
154
301
  kubun_graph_document_model_links: GraphDocumentModelTable;
155
302
  kubun_graph_models: GraphModelTable;
156
303
  kubun_graph_mutation_log: MutationLogTable;
304
+ kubun_graph_pending_groups: {
305
+ group_id: string;
306
+ };
157
307
  kubun_graph_search_models: SearchModelTable;
158
308
  kubun_graph_user_model_access_defaults: UserModelAccessDefaultTable;
159
309
  [key: `k_${string}`]: DocumentTable;
@@ -165,6 +315,7 @@ export type DocumentParams = {
165
315
  export type CreateDocumentParams = DocumentParams & {
166
316
  owner: string;
167
317
  unique: Uint8Array;
318
+ authoredHere?: boolean;
168
319
  };
169
320
  export type SaveDocumentParams = DocumentParams & {
170
321
  existing: DocumentNode;
@@ -195,7 +346,7 @@ export type CreateGraphParams = {
195
346
  export type AddDocumentModelParams = {
196
347
  id: string;
197
348
  graphModelID: string;
198
- model: import('@kubun/protocol').DocumentModel;
349
+ model: DocumentModel;
199
350
  };
200
351
  export type GraphModelWithRecord = GraphModel & {
201
352
  record: DocumentModelsRecord;
@@ -215,7 +366,10 @@ export type QueryDocumentsParams = ConnectionArguments & {
215
366
  filter?: DocumentFilter;
216
367
  orderBy?: DocumentOrderBy;
217
368
  owner?: string;
218
- catalogID?: string;
369
+ catalogRef?: {
370
+ owner: string;
371
+ id: string;
372
+ };
219
373
  };
220
374
  export type CursorDocument = {
221
375
  cursor: string;
@@ -238,6 +392,10 @@ export type SearchDocumentsParams = {
238
392
  modelIDs: Array<string>;
239
393
  first?: number;
240
394
  offset?: number;
395
+ catalogRef?: {
396
+ owner: string;
397
+ id: string;
398
+ };
241
399
  };
242
400
  export type SearchDocumentResult = {
243
401
  documentID: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/store-graph",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,17 +15,21 @@
15
15
  "LICENSE.md"
16
16
  ],
17
17
  "dependencies": {
18
- "@kubun/db": "^0.17.0",
19
- "@kubun/db-adapter": "^0.17.0",
20
- "@kubun/id": "^0.17.0",
21
- "@kubun/protocol": "^0.17.0",
22
- "@sozai/codec": "^0.4.0",
23
- "kysely": "^0.29.5"
18
+ "@kokuin/token": "^0.5.1",
19
+ "@kubun/db": "^0.18.0",
20
+ "@kubun/db-adapter": "^0.18.0",
21
+ "@kubun/id": "^0.18.0",
22
+ "@kubun/mutation": "^0.18.0",
23
+ "@kubun/protocol": "^0.18.0",
24
+ "@kubun/records": "^0.18.0",
25
+ "@noble/hashes": "^2.4.0",
26
+ "@sozai/codec": "^0.4.3",
27
+ "kysely": "^0.29.7"
24
28
  },
25
29
  "devDependencies": {
26
- "@kubun/db-better-sqlite": "^0.17.0",
27
- "@kubun/db-postgres": "^0.17.0",
28
- "@testcontainers/postgresql": "^12.1.0"
30
+ "@kubun/db-better-sqlite": "^0.18.0",
31
+ "@kubun/db-postgres": "^0.18.0",
32
+ "@testcontainers/postgresql": "^12.2.1"
29
33
  },
30
34
  "publishConfig": {
31
35
  "access": "public"