@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.
@@ -0,0 +1,120 @@
1
+ import { type DocumentNode, parseCatalogDuration } from '@kubun/protocol';
2
+ import type { GraphStoreAPI } from './api.js';
3
+ export type CatalogRef = {
4
+ owner: string;
5
+ id: string;
6
+ };
7
+ export type CatalogRuleInput = Omit<CatalogRule, 'id'> & {
8
+ id?: string;
9
+ };
10
+ export type CatalogRule = {
11
+ id: string;
12
+ models?: Array<string>;
13
+ owners?: Array<string>;
14
+ maxAge?: string;
15
+ maxAhead?: string;
16
+ dateField?: string;
17
+ };
18
+ export type CatalogLevel = 'KNOWN' | 'OFFLINE' | 'SERVE_ONLY';
19
+ export type CatalogUse = {
20
+ level: CatalogLevel | null;
21
+ followed: boolean;
22
+ };
23
+ export type RouteRulesInput = ReadonlyArray<{
24
+ catalog: CatalogRef;
25
+ rules: ReadonlyArray<CatalogRule>;
26
+ }>;
27
+ export type DocumentHold = {
28
+ id: string;
29
+ documentID: string;
30
+ createdBy: string;
31
+ reason: string | null;
32
+ expiresAt: number | null;
33
+ };
34
+ export type PlaceDocumentHoldParams = {
35
+ holdID: string;
36
+ documentID: string;
37
+ createdBy: string;
38
+ reason?: string;
39
+ expiresAt?: number;
40
+ };
41
+ export type DocumentScope = {
42
+ scope: 'visible' | 'retained' | 'outOfScope';
43
+ retained: boolean;
44
+ refs: Array<string>;
45
+ };
46
+ /**
47
+ * `LOCAL`: the document is here. `UNAVAILABLE`: this device swept it and a retaining rule matches
48
+ * it again. Null: neither, the same as a document never received.
49
+ */
50
+ export type DocumentAvailability = 'LOCAL' | 'UNAVAILABLE';
51
+ /** A document's scope with its availability, as an app reads it. */
52
+ export type DocumentScopeView = DocumentScope & {
53
+ availability: DocumentAvailability | null;
54
+ };
55
+ /** A swept document a retaining rule matches again, with the catalogs whose rules match it. */
56
+ export type UnavailableDocument = {
57
+ documentID: string;
58
+ owner: string;
59
+ catalogs: Array<CatalogRef>;
60
+ };
61
+ /** What this device kept of a document it swept. */
62
+ export type SweptDocument = {
63
+ documentID: string;
64
+ modelID: string;
65
+ owner: string;
66
+ createdAt: number;
67
+ scopeDate: number | null;
68
+ /** Each declared date field's scope date. */
69
+ fieldDates: Record<string, number>;
70
+ /** The document's own read rule, when it had one. */
71
+ readAccess: SweptReadAccess | null;
72
+ mutationDigest: string;
73
+ sweptAt: number;
74
+ };
75
+ export type SweptReadAccess = {
76
+ level: AccessLevelName;
77
+ allowedDIDs?: Array<string>;
78
+ allowedGroups?: Array<string>;
79
+ allowedCircles?: Array<string>;
80
+ };
81
+ type AccessLevelName = 'only_owner' | 'anyone' | 'restricted';
82
+ export type ScopeCatalog = {
83
+ name: string;
84
+ rules: Array<CatalogRule>;
85
+ };
86
+ export declare class InvalidCatalogScopeError extends Error {
87
+ get code(): 'INVALID_CATALOG_RULE';
88
+ constructor(message: string, options?: ErrorOptions);
89
+ }
90
+ export declare class CatalogScopeRuleLimitError extends Error {
91
+ get code(): 'CATALOG_RULE_LIMIT_EXCEEDED';
92
+ constructor();
93
+ }
94
+ export declare class CatalogScopeRulesEmptyError extends Error {
95
+ get code(): 'CATALOG_RULES_EMPTY';
96
+ constructor();
97
+ }
98
+ export declare const parseScopeDuration: typeof parseCatalogDuration;
99
+ export declare function isCatalogRuleID(value: unknown): value is string;
100
+ export declare function validateScopeCatalog(catalog: ScopeCatalog): void;
101
+ export declare function parseScopeDate(value: unknown): number | null;
102
+ export declare function scopeRuleMatches(rule: CatalogRule, doc: DocumentNode, nowMS: number, interfaces?: ReadonlyArray<string>): boolean;
103
+ /** What a catalog rule reads of a document, for one that may no longer be stored. */
104
+ export type ScopeRuleFacts = {
105
+ model: string;
106
+ owner: string;
107
+ /** The scope date a rule windows on: the model's own with no field, else the field's. */
108
+ dateOf: (field: string | null) => number;
109
+ };
110
+ export declare function scopeRuleMatchesFacts(rule: CatalogRule, facts: ScopeRuleFacts, nowMS: number, interfaces?: ReadonlyArray<string>): boolean;
111
+ export declare function recomputeDocumentRefs(documentID: string, tx: GraphStoreAPI, routeRules: RouteRulesInput): Promise<DocumentScope>;
112
+ export declare function canonicalCatalogRuleContent(rule: CatalogRuleInput): string;
113
+ export type AssignCatalogRuleIDsParams = {
114
+ input: ReadonlyArray<CatalogRuleInput>;
115
+ current: ReadonlyArray<CatalogRule>;
116
+ ancestryRuleIDs: ReadonlySet<string>;
117
+ newRuleID: () => string;
118
+ };
119
+ export declare function assignCatalogRuleIDs(params: AssignCatalogRuleIDsParams): Array<CatalogRule>;
120
+ export {};
@@ -0,0 +1,165 @@
1
+ import { InvalidCatalogDurationError, parseCatalogDuration } from '@kubun/protocol';
2
+ export class InvalidCatalogScopeError extends Error {
3
+ get code() {
4
+ return 'INVALID_CATALOG_RULE';
5
+ }
6
+ constructor(message, options){
7
+ super(message, options);
8
+ this.name = 'InvalidCatalogScopeError';
9
+ }
10
+ }
11
+ export class CatalogScopeRuleLimitError extends Error {
12
+ get code() {
13
+ return 'CATALOG_RULE_LIMIT_EXCEEDED';
14
+ }
15
+ constructor(){
16
+ super('Catalogs must have between 1 and 32 rules');
17
+ this.name = 'CatalogScopeRuleLimitError';
18
+ }
19
+ }
20
+ export class CatalogScopeRulesEmptyError extends Error {
21
+ get code() {
22
+ return 'CATALOG_RULES_EMPTY';
23
+ }
24
+ constructor(){
25
+ super('Catalogs must have at least one rule');
26
+ this.name = 'CatalogScopeRulesEmptyError';
27
+ }
28
+ }
29
+ export const parseScopeDuration = parseCatalogDuration;
30
+ export function isCatalogRuleID(value) {
31
+ return typeof value === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(value);
32
+ }
33
+ export function validateScopeCatalog(catalog) {
34
+ if (catalog.rules.length === 0) throw new CatalogScopeRulesEmptyError();
35
+ if (catalog.rules.length > 32) {
36
+ throw new CatalogScopeRuleLimitError();
37
+ }
38
+ for (const rule of catalog.rules){
39
+ if (!isCatalogRuleID(rule.id)) {
40
+ throw new InvalidCatalogScopeError('Invalid catalog rule ID');
41
+ }
42
+ if (rule.models?.length === 0 || rule.owners?.length === 0) {
43
+ throw new InvalidCatalogScopeError('Catalog selectors must be nonempty');
44
+ }
45
+ if (rule.models == null && rule.owners == null && rule.maxAge == null && rule.maxAhead == null) {
46
+ throw new InvalidCatalogScopeError('A rule needs a model, owner or window selector');
47
+ }
48
+ try {
49
+ if (rule.maxAge != null) parseScopeDuration(rule.maxAge);
50
+ if (rule.maxAhead != null) parseScopeDuration(rule.maxAhead);
51
+ } catch (error) {
52
+ if (error instanceof InvalidCatalogDurationError) {
53
+ throw new InvalidCatalogScopeError(error.message, {
54
+ cause: error
55
+ });
56
+ }
57
+ throw error;
58
+ }
59
+ if (rule.dateField != null && (rule.models == null || rule.models.length === 0)) {
60
+ throw new InvalidCatalogScopeError('A date-field override requires selected models');
61
+ }
62
+ }
63
+ }
64
+ const fullDatePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
65
+ const dateTimePattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
66
+ export function parseScopeDate(value) {
67
+ if (typeof value !== 'string') return null;
68
+ const full = fullDatePattern.exec(value);
69
+ const timed = dateTimePattern.exec(value);
70
+ const match = full ?? timed;
71
+ if (match == null) return null;
72
+ const year = Number(match[1]);
73
+ const month = Number(match[2]);
74
+ const day = Number(match[3]);
75
+ const validDay = new Date(Date.UTC(year, month - 1, day));
76
+ if (validDay.getUTCFullYear() !== year || validDay.getUTCMonth() + 1 !== month || validDay.getUTCDate() !== day) return null;
77
+ if (timed != null) {
78
+ if (Number(timed[4]) > 23 || Number(timed[5]) > 59 || Number(timed[6]) > 59) return null;
79
+ const offset = timed[7] ?? 'Z';
80
+ if (offset !== 'Z' && (Number(offset.slice(1, 3)) > 23 || Number(offset.slice(4)) > 59)) {
81
+ return null;
82
+ }
83
+ }
84
+ const timestamp = Date.parse(value);
85
+ return Number.isFinite(timestamp) ? timestamp : null;
86
+ }
87
+ function fieldValue(data, field) {
88
+ let value = data;
89
+ for (const segment of field.split('.')){
90
+ if (value == null || typeof value !== 'object') return null;
91
+ value = value[segment];
92
+ }
93
+ return value;
94
+ }
95
+ export function scopeRuleMatches(rule, doc, nowMS, interfaces = []) {
96
+ const createdAt = doc.createdAt.getTime();
97
+ return scopeRuleMatchesFacts(rule, {
98
+ model: doc.model,
99
+ owner: doc.owner,
100
+ dateOf: (field)=>field == null ? doc.scopeDate ?? createdAt : parseScopeDate(fieldValue(doc.data, field)) ?? createdAt
101
+ }, nowMS, interfaces);
102
+ }
103
+ export function scopeRuleMatchesFacts(rule, facts, nowMS, interfaces = []) {
104
+ if (rule.models != null && !rule.models.some((model)=>model === facts.model || interfaces.includes(model))) {
105
+ return false;
106
+ }
107
+ if (rule.owners != null && !rule.owners.includes(facts.owner)) return false;
108
+ if (rule.maxAge == null && rule.maxAhead == null) return true;
109
+ const date = facts.dateOf(rule.dateField ?? null);
110
+ if (rule.maxAge != null && date < nowMS - parseScopeDuration(rule.maxAge)) return false;
111
+ if (rule.maxAhead != null && date >= nowMS + parseScopeDuration(rule.maxAhead)) return false;
112
+ return true;
113
+ }
114
+ export function recomputeDocumentRefs(documentID, tx, routeRules) {
115
+ return tx.recomputeDocumentRefs({
116
+ documentID,
117
+ routeRules
118
+ });
119
+ }
120
+ export function canonicalCatalogRuleContent(rule) {
121
+ return JSON.stringify({
122
+ models: rule.models == null ? null : [
123
+ ...new Set(rule.models)
124
+ ].sort(),
125
+ owners: rule.owners == null ? null : [
126
+ ...new Set(rule.owners)
127
+ ].sort(),
128
+ maxAge: rule.maxAge ?? null,
129
+ maxAhead: rule.maxAhead ?? null,
130
+ dateField: rule.dateField ?? null
131
+ });
132
+ }
133
+ export function assignCatalogRuleIDs(params) {
134
+ const claimed = new Set();
135
+ const admissible = new Set([
136
+ ...params.ancestryRuleIDs,
137
+ ...params.current.map((rule)=>rule.id)
138
+ ]);
139
+ for (const rule of params.input){
140
+ if (rule.id === undefined) continue;
141
+ if (!isCatalogRuleID(rule.id)) {
142
+ throw new InvalidCatalogScopeError('Invalid catalog rule ID');
143
+ }
144
+ if (claimed.has(rule.id)) throw new InvalidCatalogScopeError('Duplicate catalog rule ID');
145
+ if (!admissible.has(rule.id)) throw new InvalidCatalogScopeError('Catalog rule ID does not belong to this lineage');
146
+ claimed.add(rule.id);
147
+ }
148
+ return params.input.map((rule)=>{
149
+ if (rule.id !== undefined) return {
150
+ ...rule,
151
+ id: rule.id
152
+ };
153
+ const content = canonicalCatalogRuleContent(rule);
154
+ const match = params.current.find((candidate)=>!claimed.has(candidate.id) && canonicalCatalogRuleContent(candidate) === content);
155
+ const id = match?.id ?? params.newRuleID();
156
+ if (claimed.has(id) || match == null && admissible.has(id)) {
157
+ throw new InvalidCatalogScopeError('Could not allocate a unique catalog rule ID');
158
+ }
159
+ claimed.add(id);
160
+ return {
161
+ ...rule,
162
+ id
163
+ };
164
+ });
165
+ }
@@ -0,0 +1,21 @@
1
+ import type { SignedRecord } from '@kubun/records';
2
+ import type { Kysely } from 'kysely';
3
+ import type { CatalogDraftState, CatalogVersionRef, LocalCatalogPublication, PendingCatalogHead } from './catalog-types.js';
4
+ import type { GraphTables } from './tables.js';
5
+ export type CatalogStateMethods = {
6
+ getAdmittedCatalogHead(catalogID: string): Promise<SignedRecord | null>;
7
+ putAdmittedCatalogHead(catalogID: string, record: SignedRecord): Promise<void>;
8
+ deleteAdmittedCatalogHead(catalogID: string): Promise<void>;
9
+ getCatalogDraft(catalogID: string): Promise<CatalogDraftState | null>;
10
+ putCatalogDraft(draft: CatalogDraftState): Promise<void>;
11
+ deleteCatalogDraft(catalogID: string): Promise<boolean>;
12
+ putLocalCatalogPublication(value: LocalCatalogPublication): Promise<void>;
13
+ listLocalCatalogPublications(catalogID: string): Promise<Array<LocalCatalogPublication>>;
14
+ getRecipientCatalogVersion(ref: CatalogVersionRef): Promise<SignedRecord | null>;
15
+ putRecipientCatalogVersion(ref: CatalogVersionRef, record: SignedRecord): Promise<void>;
16
+ getPendingCatalogHead(catalogID: string): Promise<PendingCatalogHead | null>;
17
+ putPendingCatalogHead(head: PendingCatalogHead): Promise<void>;
18
+ deletePendingCatalogHead(catalogID: string): Promise<void>;
19
+ listPendingCatalogHeads(): Promise<Array<PendingCatalogHead>>;
20
+ };
21
+ export declare function createCatalogStateMethods(db: Kysely<GraphTables>): CatalogStateMethods;
@@ -0,0 +1,122 @@
1
+ import { fromB64U, toB64U } from '@sozai/codec';
2
+ function normalizeBytes(value) {
3
+ if (value instanceof Uint8Array) return {
4
+ bytes: toB64U(value)
5
+ };
6
+ if (Array.isArray(value)) return value.map(normalizeBytes);
7
+ if (value !== null && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item])=>[
8
+ key,
9
+ normalizeBytes(item)
10
+ ]));
11
+ return value;
12
+ }
13
+ function encode(value) {
14
+ // Buffer.toJSON runs before a replacer; normalize bytes before JSON visits them.
15
+ return JSON.stringify(normalizeBytes(value));
16
+ }
17
+ function decode(value) {
18
+ return JSON.parse(typeof value === 'string' ? value : JSON.stringify(value), (_key, item)=>{
19
+ if (item !== null && typeof item === 'object' && 'bytes' in item && Object.keys(item).length === 1 && typeof item.bytes === 'string') return fromB64U(item.bytes);
20
+ return item;
21
+ });
22
+ }
23
+ function pending(row) {
24
+ return {
25
+ catalogID: row.catalog_id,
26
+ headVersionID: row.head_version_id,
27
+ groupID: row.group_id,
28
+ retryAt: Number(row.retry_at),
29
+ attempts: Number(row.attempts)
30
+ };
31
+ }
32
+ export function createCatalogStateMethods(db) {
33
+ return {
34
+ async getAdmittedCatalogHead (catalogID) {
35
+ const row = await db.selectFrom('kubun_graph_catalog_admitted_heads').select('signed_record').where('catalog_id', '=', catalogID).executeTakeFirst();
36
+ return row === undefined ? null : decode(row.signed_record);
37
+ },
38
+ async putAdmittedCatalogHead (catalogID, record) {
39
+ const values = {
40
+ catalog_id: catalogID,
41
+ signed_record: encode(record)
42
+ };
43
+ await db.insertInto('kubun_graph_catalog_admitted_heads').values(values).onConflict((oc)=>oc.column('catalog_id').doUpdateSet({
44
+ signed_record: values.signed_record
45
+ })).execute();
46
+ },
47
+ async deleteAdmittedCatalogHead (catalogID) {
48
+ await db.deleteFrom('kubun_graph_catalog_admitted_heads').where('catalog_id', '=', catalogID).execute();
49
+ },
50
+ async getCatalogDraft (catalogID) {
51
+ const row = await db.selectFrom('kubun_graph_catalog_drafts').select('state').where('catalog_id', '=', catalogID).executeTakeFirst();
52
+ return row === undefined ? null : decode(row.state);
53
+ },
54
+ async putCatalogDraft (draft) {
55
+ const values = {
56
+ catalog_id: draft.catalogID,
57
+ state: encode(draft)
58
+ };
59
+ await db.insertInto('kubun_graph_catalog_drafts').values(values).onConflict((oc)=>oc.column('catalog_id').doUpdateSet({
60
+ state: values.state
61
+ })).execute();
62
+ },
63
+ async deleteCatalogDraft (catalogID) {
64
+ const result = await db.deleteFrom('kubun_graph_catalog_drafts').where('catalog_id', '=', catalogID).executeTakeFirst();
65
+ return Number(result.numDeletedRows) > 0;
66
+ },
67
+ async putLocalCatalogPublication (value) {
68
+ await db.insertInto('kubun_graph_catalog_publications').values({
69
+ catalog_id: value.catalogID,
70
+ version_id: value.versionID
71
+ }).onConflict((oc)=>oc.columns([
72
+ 'catalog_id',
73
+ 'version_id'
74
+ ]).doNothing()).execute();
75
+ },
76
+ async listLocalCatalogPublications (catalogID) {
77
+ const rows = await db.selectFrom('kubun_graph_catalog_publications').selectAll().where('catalog_id', '=', catalogID).orderBy('version_id').execute();
78
+ return rows.map((row)=>({
79
+ catalogID: row.catalog_id,
80
+ versionID: row.version_id
81
+ }));
82
+ },
83
+ async getRecipientCatalogVersion (ref) {
84
+ const row = await db.selectFrom('kubun_graph_catalog_recipient_versions').select('signed_record').where('catalog_id', '=', ref.catalogID).where('version_id', '=', ref.versionID).executeTakeFirst();
85
+ return row === undefined ? null : decode(row.signed_record);
86
+ },
87
+ async putRecipientCatalogVersion (ref, record) {
88
+ const values = {
89
+ catalog_id: ref.catalogID,
90
+ version_id: ref.versionID,
91
+ signed_record: encode(record)
92
+ };
93
+ await db.insertInto('kubun_graph_catalog_recipient_versions').values(values).onConflict((oc)=>oc.columns([
94
+ 'catalog_id',
95
+ 'version_id'
96
+ ]).doUpdateSet({
97
+ signed_record: values.signed_record
98
+ })).execute();
99
+ },
100
+ async getPendingCatalogHead (catalogID) {
101
+ const row = await db.selectFrom('kubun_graph_catalog_pending_heads').selectAll().where('catalog_id', '=', catalogID).executeTakeFirst();
102
+ return row === undefined ? null : pending(row);
103
+ },
104
+ async putPendingCatalogHead (head) {
105
+ const values = {
106
+ catalog_id: head.catalogID,
107
+ head_version_id: head.headVersionID,
108
+ group_id: head.groupID,
109
+ retry_at: head.retryAt,
110
+ attempts: head.attempts
111
+ };
112
+ await db.insertInto('kubun_graph_catalog_pending_heads').values(values).onConflict((oc)=>oc.column('catalog_id').doUpdateSet(values)).execute();
113
+ },
114
+ async deletePendingCatalogHead (catalogID) {
115
+ await db.deleteFrom('kubun_graph_catalog_pending_heads').where('catalog_id', '=', catalogID).execute();
116
+ },
117
+ async listPendingCatalogHeads () {
118
+ const rows = await db.selectFrom('kubun_graph_catalog_pending_heads').selectAll().orderBy('catalog_id').execute();
119
+ return rows.map(pending);
120
+ }
121
+ };
122
+ }
@@ -0,0 +1,71 @@
1
+ import type { SignedRecord } from '@kubun/records';
2
+ import type { CatalogRule } from './catalog-scope.js';
3
+ export type CatalogDefinition = {
4
+ name: string;
5
+ description?: string;
6
+ app?: string;
7
+ rules: Array<CatalogRule>;
8
+ };
9
+ export type CatalogVersionRef = {
10
+ catalogID: string;
11
+ versionID: string;
12
+ };
13
+ export type CatalogVersionPayload = {
14
+ definition: CatalogDefinition;
15
+ parents: Array<CatalogVersionRef>;
16
+ upstream?: CatalogVersionRef;
17
+ embeddedBase?: SignedRecord;
18
+ publishedAt: string;
19
+ };
20
+ export type CatalogHeadPayload = CatalogDefinition & {
21
+ headVersionID: string;
22
+ upstream?: CatalogVersionRef;
23
+ };
24
+ export type CatalogRuleResolutionInput = {
25
+ ruleID: string;
26
+ side: 'LOCAL' | 'UPSTREAM';
27
+ };
28
+ export type OwnerCatalogMergeState = {
29
+ baseVersionID: string;
30
+ localRevisionID: string;
31
+ upstreamVersionID: string;
32
+ winningHeadVersionID: string;
33
+ };
34
+ export type CatalogDraftState = {
35
+ catalogID: string;
36
+ baseVersionID: string;
37
+ definition: CatalogDefinition;
38
+ dirtyRuleIDs: Array<string>;
39
+ resolutions: Array<CatalogRuleResolutionInput>;
40
+ integratedBase: SignedRecord | null;
41
+ mergedBranchVersionIDs: Array<string>;
42
+ activeBranchVersionID: string | null;
43
+ activeOwnerMerge: OwnerCatalogMergeState | null;
44
+ };
45
+ export type LocalCatalogPublication = {
46
+ catalogID: string;
47
+ versionID: string;
48
+ };
49
+ export type PendingCatalogHead = {
50
+ catalogID: string;
51
+ headVersionID: string;
52
+ groupID: string | null;
53
+ retryAt: number;
54
+ attempts: number;
55
+ };
56
+ export type CatalogRuleConflict = {
57
+ ruleID: string;
58
+ base: CatalogRule | null;
59
+ local: CatalogRule | null;
60
+ upstream: CatalogRule | null;
61
+ };
62
+ export type MergeCatalogDefinitionsParams = {
63
+ base: CatalogDefinition;
64
+ local: CatalogDefinition;
65
+ upstream: CatalogDefinition;
66
+ resolutions?: ReadonlyArray<CatalogRuleResolutionInput>;
67
+ };
68
+ export type CatalogMergeResult = {
69
+ merged: CatalogDefinition;
70
+ conflicts: Array<CatalogRuleConflict>;
71
+ };
@@ -0,0 +1 @@
1
+ export { };
package/lib/errors.d.ts CHANGED
@@ -32,3 +32,18 @@ export declare class ModelNotDeployedError extends Error {
32
32
  modelID: string;
33
33
  };
34
34
  }
35
+ export declare class InvalidModelDateFieldError extends Error {
36
+ get code(): 'INVALID_MODEL_DATE_FIELD';
37
+ constructor(message: string);
38
+ }
39
+ export declare class MissingGraphCommitContextError extends Error {
40
+ constructor();
41
+ }
42
+ export declare class DocumentNotHoldableError extends Error {
43
+ get code(): string;
44
+ constructor(options?: ErrorOptions);
45
+ }
46
+ export declare class DocumentLockRequiresTransactionError extends Error {
47
+ get code(): 'GRAPH_DOCUMENT_LOCK_REQUIRES_TRANSACTION';
48
+ constructor();
49
+ }
package/lib/errors.js CHANGED
@@ -43,3 +43,36 @@
43
43
  };
44
44
  }
45
45
  }
46
+ export class InvalidModelDateFieldError extends Error {
47
+ get code() {
48
+ return 'INVALID_MODEL_DATE_FIELD';
49
+ }
50
+ constructor(message){
51
+ super(message);
52
+ this.name = 'InvalidModelDateFieldError';
53
+ }
54
+ }
55
+ export class MissingGraphCommitContextError extends Error {
56
+ constructor(){
57
+ super('Graph store requires a commit context');
58
+ this.name = 'MissingGraphCommitContextError';
59
+ }
60
+ }
61
+ export class DocumentNotHoldableError extends Error {
62
+ get code() {
63
+ return 'DOCUMENT_NOT_HOLDABLE';
64
+ }
65
+ constructor(options){
66
+ super('Only a local document the viewer can read can be held', options);
67
+ this.name = 'DocumentNotHoldableError';
68
+ }
69
+ }
70
+ export class DocumentLockRequiresTransactionError extends Error {
71
+ get code() {
72
+ return 'GRAPH_DOCUMENT_LOCK_REQUIRES_TRANSACTION';
73
+ }
74
+ constructor(){
75
+ super('A document row lock is held only by a transaction');
76
+ this.name = 'DocumentLockRequiresTransactionError';
77
+ }
78
+ }
@@ -0,0 +1,7 @@
1
+ export declare const MAX_IDENTIFIER_BYTES = 63;
2
+ /**
3
+ * Returns an identifier of at most `MAX_IDENTIFIER_BYTES` bytes. Names within the
4
+ * limit are returned unchanged; longer ones keep their leading part and end with
5
+ * a hash of the full name, so names that differ only past the limit stay distinct.
6
+ */
7
+ export declare function boundedIdentifier(name: string): string;
@@ -0,0 +1,27 @@
1
+ // Postgres truncates identifiers longer than NAMEDATALEN - 1 bytes, so two long
2
+ // names sharing a prefix can silently resolve to the same object.
3
+ export const MAX_IDENTIFIER_BYTES = 63;
4
+ const HASH_LENGTH = 13 // a 64-bit value in base36
5
+ ;
6
+ const FNV_OFFSET = 0xcbf29ce484222325n;
7
+ const FNV_PRIME = 0x100000001b3n;
8
+ const UINT64_MASK = 0xffffffffffffffffn;
9
+ function hashName(bytes) {
10
+ let hash = FNV_OFFSET;
11
+ for (const byte of bytes){
12
+ hash = (hash ^ BigInt(byte)) * FNV_PRIME & UINT64_MASK;
13
+ }
14
+ return hash.toString(36).padStart(HASH_LENGTH, '0');
15
+ }
16
+ /**
17
+ * Returns an identifier of at most `MAX_IDENTIFIER_BYTES` bytes. Names within the
18
+ * limit are returned unchanged; longer ones keep their leading part and end with
19
+ * a hash of the full name, so names that differ only past the limit stay distinct.
20
+ */ export function boundedIdentifier(name) {
21
+ const bytes = new TextEncoder().encode(name);
22
+ if (bytes.length <= MAX_IDENTIFIER_BYTES) {
23
+ return name;
24
+ }
25
+ const prefix = name.slice(0, MAX_IDENTIFIER_BYTES - HASH_LENGTH - 1);
26
+ return `${prefix}_${hashName(bytes)}`;
27
+ }
package/lib/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
+ export { accessDefaultRecordProvider, GraphAccessDefaultRecordError, GraphAccessDefaultRecordTypeError, graphAccessDefaultRecordAdapter, } from './access-default-records.js';
1
2
  import type { StoreProvider } from '@kubun/db';
2
3
  import type { GraphStoreAPI } from './api.js';
3
- export type { GraphStoreAPI, StoredAccessDefaultRow, StoredAccessRule } from './api.js';
4
- export { createGraphStore } from './api.js';
4
+ export type { CustodyNonce, CustodyPinRelease, CustodyReceiptRecord, DocumentSweepRefusal, DocumentSweepResult, GetDocumentIDsForScopeParams, GraphStoreAPI, ReadWinnerStateDigestsParams, RecomputeDocumentRefsParams, ReconcileCatalogModelRefsParams, RecordPendingMutationAttemptParams, RemoveUserModelAccessDefaultsParams, StoredAccessDefaultRow, StoredAccessRule, SweepDocumentParams, UpdateSearchEntryParams, } from './api.js';
5
+ export { catalogRuleRef, createGraphStore, custodyPinRef, isCatalogRuleRef, isCustodyPinRef, mutationIDsDigest, readWinnerStateDigest, readWinnerStateDigests, } from './api.js';
6
+ export { catalogLocalRevisionID, mergeCatalogDefinitions, orderMergedCatalogRules, } from './catalog-merge.js';
7
+ export { catalogRecordPolicy, GraphCatalogRecordError, GraphCatalogRecordTypeError, graphCatalogRecordAdapter, graphStoreOfCatalogRecords, verifyGraphCatalogRecord, } from './catalog-records.js';
8
+ export type { AssignCatalogRuleIDsParams, CatalogLevel, CatalogRef as ScopeCatalogRef, CatalogRule as ScopeCatalogRule, CatalogRuleInput, CatalogRuleInput as ScopeCatalogRuleInput, CatalogUse, DocumentAvailability, DocumentHold, DocumentScope, DocumentScopeView, PlaceDocumentHoldParams, RouteRulesInput, ScopeCatalog, SweptDocument, SweptReadAccess, UnavailableDocument, } from './catalog-scope.js';
9
+ export { assignCatalogRuleIDs, canonicalCatalogRuleContent, isCatalogRuleID, parseScopeDate, recomputeDocumentRefs, } from './catalog-scope.js';
5
10
  export { graphStoreDefinition } from './definition.js';
6
- export { ModelNotDeployedError } from './errors.js';
11
+ export { DocumentLockRequiresTransactionError, DocumentNotHoldableError, InvalidModelDateFieldError, MissingGraphCommitContextError, ModelNotDeployedError, } from './errors.js';
7
12
  export { resolveSearchableFields, SearchNotEnabledError } from './search.js';
8
13
  export declare const GRAPH_STORE: "graph";
9
14
  export declare function getGraphStore(provider: StoreProvider): Promise<GraphStoreAPI>;
10
- export type { AccessLevel, AccessPermissions, AddDocumentModelParams, Catalog, ClusterDefinitionRow, ClusterModel, ConnectionArguments, CreateDocumentParams, CreateGraphParams, CursorDocument, Document, DocumentData, DocumentModelRow, DocumentParams, GraphModel, GraphModelWithRecord, GraphTables, InsertCatalog, InsertClusterModel, InsertDocument, InsertDocumentModel, InsertDocumentModelInterface, InsertMutationLogEntry, ListDocumentsParams, MutationLogEntry, PaginatedResult, PaginationParams, QueryDocumentsParams, QueryDocumentsResult, SaveDocumentParams, SearchConfig, SearchDocumentResult, SearchDocumentsParams, UpdateDocument, UpdateDocumentModel, ViewerReadAccess, } from './tables.js';
15
+ export { type GCMark, gcMarkPastGrace } from './api.js';
16
+ export type { CatalogDefinition, CatalogDraftState, CatalogHeadPayload, CatalogMergeResult, CatalogRuleConflict, CatalogRuleResolutionInput, CatalogVersionPayload, CatalogVersionRef, LocalCatalogPublication, MergeCatalogDefinitionsParams, OwnerCatalogMergeState, PendingCatalogHead, } from './catalog-types.js';
17
+ export type { AccessLevel, AccessPermissions, AddDocumentModelParams, ClusterDefinitionRow, ClusterModel, ConnectionArguments, CreateDocumentParams, CreateGraphParams, CursorDocument, Document, DocumentData, DocumentLeafPageParams, DocumentLeafRow, DocumentLeafRule, DocumentModelRow, DocumentParams, GraphModel, GraphModelWithRecord, GraphTables, InsertClusterModel, InsertDocument, 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,8 +1,13 @@
1
- export { createGraphStore } from './api.js';
1
+ export { accessDefaultRecordProvider, GraphAccessDefaultRecordError, GraphAccessDefaultRecordTypeError, graphAccessDefaultRecordAdapter } from './access-default-records.js';
2
+ export { catalogRuleRef, createGraphStore, custodyPinRef, isCatalogRuleRef, isCustodyPinRef, mutationIDsDigest, readWinnerStateDigest, readWinnerStateDigests } from './api.js';
3
+ export { catalogLocalRevisionID, mergeCatalogDefinitions, orderMergedCatalogRules } from './catalog-merge.js';
4
+ export { catalogRecordPolicy, GraphCatalogRecordError, GraphCatalogRecordTypeError, graphCatalogRecordAdapter, graphStoreOfCatalogRecords, verifyGraphCatalogRecord } from './catalog-records.js';
5
+ export { assignCatalogRuleIDs, canonicalCatalogRuleContent, isCatalogRuleID, parseScopeDate, recomputeDocumentRefs } from './catalog-scope.js';
2
6
  export { graphStoreDefinition } from './definition.js';
3
- export { ModelNotDeployedError } from './errors.js';
7
+ export { DocumentLockRequiresTransactionError, DocumentNotHoldableError, InvalidModelDateFieldError, MissingGraphCommitContextError, ModelNotDeployedError } from './errors.js';
4
8
  export { resolveSearchableFields, SearchNotEnabledError } from './search.js';
5
9
  export const GRAPH_STORE = 'graph';
6
10
  export function getGraphStore(provider) {
7
11
  return provider.getStore(GRAPH_STORE);
8
12
  }
13
+ export { gcMarkPastGrace } from './api.js';
@@ -1,16 +1,14 @@
1
1
  import type { MigrationContext } from '@kubun/db';
2
2
  import type { Migration } from 'kysely/migration';
3
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.
4
+ * A shipped migration should be immutable: once a database has run it, editing
5
+ * it changes nothing on that database and silently diverges its schema from the
6
+ * code. Schema changes after a release that preserves databases need a NEW
7
+ * migration.
8
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.
9
+ * `0-init` was edited in place again for the pending arrival group, sender, retry columns and
10
+ * due index. Existing databases will not acquire them and must be recreated.
11
+ * This is a breaking reset. Do NOT repeat an in-place edit once a release that
12
+ * must preserve existing databases ships; add a new migration then.
15
13
  */
16
14
  export declare function getGraphMigrations(ctx: MigrationContext): Record<string, Migration>;