@kubun/store-graph 0.17.1 → 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('provider_merge_policy', t.json).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>>;
@@ -30,6 +33,8 @@ export type DocumentModelTable = {
30
33
  provider_merge_policy: JSONValueColumn<{
31
34
  group: Array<string>;
32
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>;
33
38
  created_at: CreatedAtColumn;
34
39
  updated_at: UpdatedAtColumn;
35
40
  };
@@ -74,13 +79,139 @@ export type MutationLogTable = {
74
79
  mutation_hash: string;
75
80
  model_id: string;
76
81
  document_id: string;
82
+ /** Unverified signed issuer, informational only. Caps and authorisation use sender_peer_did. */
77
83
  author_did: string;
78
84
  hlc: string;
79
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>;
80
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>;
81
93
  };
82
94
  export type MutationLogEntry = Selectable<MutationLogTable>;
83
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
+ };
84
215
  export type ClusterModelTable = {
85
216
  model_id: string;
86
217
  cluster_id: string;
@@ -111,45 +242,58 @@ export type UserModelAccessDefaultTable = {
111
242
  export type UserModelAccessDefault = Selectable<UserModelAccessDefaultTable>;
112
243
  export type InsertUserModelAccessDefault = Insertable<UserModelAccessDefaultTable>;
113
244
  export type UpdateUserModelAccessDefault = Updateable<UserModelAccessDefaultTable>;
114
- export type CatalogTable = {
245
+ export type GraphCatalogRecordTable = {
246
+ type: string;
247
+ owner: string;
115
248
  id: string;
116
- owner_did: string;
117
- name: string;
118
- description: string;
119
- filter_criteria: JSONValueColumn<CatalogFilterCriteria>;
249
+ op_hash: string;
120
250
  hlc: string;
121
- /**
122
- * Local-only sync activation: 1 = active (scopes sync), 0/null = known (a
123
- * discovered catalog that has not been activated). Insert-optional; own
124
- * catalogs are created active, discovered ones arrive known.
125
- */
126
- active: ColumnType<number | null, number | null | undefined, number | null>;
127
- /**
128
- * Provenance: the group this catalog was first discovered from, null for own
129
- * catalogs. First-discovery only — never overwritten on re-discovery.
130
- */
131
- source_group_id: ColumnType<string | null, string | null | undefined, string | null>;
132
- /**
133
- * Provenance: the circle whose `catalog_ids` first referenced this catalog
134
- * (set only for invite-seeded discovery), null otherwise. First-discovery only.
135
- */
136
- source_circle_id: ColumnType<string | null, string | null | undefined, string | null>;
137
- /**
138
- * The creator-signed `catalog:set` token that authenticated this catalog: own
139
- * catalogs sign it at creation, discovered ones store the verified token that
140
- * delivered them. Forwarded verbatim in invite seeds so a joiner re-verifies
141
- * creator-binding (`ownerDID === iss`) with the same helper the broadcast
142
- * receive path uses. Null for rows that predate token capture.
143
- */
144
- signed_token: ColumnType<string | null, string | null | undefined, string | null>;
145
- created_at: CreatedAtColumn;
146
- 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;
147
256
  };
148
- export type Catalog = Selectable<CatalogTable>;
149
- export type InsertCatalog = Insertable<CatalogTable>;
150
- export type UpdateCatalog = Updateable<CatalogTable>;
257
+ export type GraphCatalogRecordRow = Selectable<GraphCatalogRecordTable>;
151
258
  export type GraphTables = {
152
- 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;
153
297
  kubun_graph_cluster_models: ClusterModelTable;
154
298
  kubun_graph_clusters: ClusterDefinitionTable;
155
299
  kubun_graph_document_models: DocumentModelTable;
@@ -157,6 +301,9 @@ export type GraphTables = {
157
301
  kubun_graph_document_model_links: GraphDocumentModelTable;
158
302
  kubun_graph_models: GraphModelTable;
159
303
  kubun_graph_mutation_log: MutationLogTable;
304
+ kubun_graph_pending_groups: {
305
+ group_id: string;
306
+ };
160
307
  kubun_graph_search_models: SearchModelTable;
161
308
  kubun_graph_user_model_access_defaults: UserModelAccessDefaultTable;
162
309
  [key: `k_${string}`]: DocumentTable;
@@ -168,6 +315,7 @@ export type DocumentParams = {
168
315
  export type CreateDocumentParams = DocumentParams & {
169
316
  owner: string;
170
317
  unique: Uint8Array;
318
+ authoredHere?: boolean;
171
319
  };
172
320
  export type SaveDocumentParams = DocumentParams & {
173
321
  existing: DocumentNode;
@@ -198,7 +346,7 @@ export type CreateGraphParams = {
198
346
  export type AddDocumentModelParams = {
199
347
  id: string;
200
348
  graphModelID: string;
201
- model: import('@kubun/protocol').DocumentModel;
349
+ model: DocumentModel;
202
350
  };
203
351
  export type GraphModelWithRecord = GraphModel & {
204
352
  record: DocumentModelsRecord;
@@ -218,7 +366,10 @@ export type QueryDocumentsParams = ConnectionArguments & {
218
366
  filter?: DocumentFilter;
219
367
  orderBy?: DocumentOrderBy;
220
368
  owner?: string;
221
- catalogID?: string;
369
+ catalogRef?: {
370
+ owner: string;
371
+ id: string;
372
+ };
222
373
  };
223
374
  export type CursorDocument = {
224
375
  cursor: string;
@@ -241,6 +392,10 @@ export type SearchDocumentsParams = {
241
392
  modelIDs: Array<string>;
242
393
  first?: number;
243
394
  offset?: number;
395
+ catalogRef?: {
396
+ owner: string;
397
+ id: string;
398
+ };
244
399
  };
245
400
  export type SearchDocumentResult = {
246
401
  documentID: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/store-graph",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,18 +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/mutation": "^0.17.1",
22
- "@kubun/protocol": "^0.17.1",
23
- "@sozai/codec": "^0.4.0",
24
- "kysely": "^0.29.6"
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"
25
28
  },
26
29
  "devDependencies": {
27
- "@kubun/db-better-sqlite": "^0.17.0",
28
- "@kubun/db-postgres": "^0.17.1",
29
- "@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"
30
33
  },
31
34
  "publishConfig": {
32
35
  "access": "public"