@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/access-default-records.d.ts +18 -0
- package/lib/access-default-records.js +167 -0
- package/lib/api.d.ts +411 -52
- package/lib/api.js +2427 -188
- package/lib/catalog-merge.d.ts +5 -0
- package/lib/catalog-merge.js +117 -0
- package/lib/catalog-records.d.ts +34 -0
- package/lib/catalog-records.js +527 -0
- package/lib/catalog-scope.d.ts +120 -0
- package/lib/catalog-scope.js +165 -0
- package/lib/catalog-state.d.ts +21 -0
- package/lib/catalog-state.js +122 -0
- package/lib/catalog-types.d.ts +71 -0
- package/lib/catalog-types.js +1 -0
- package/lib/errors.d.ts +15 -0
- package/lib/errors.js +33 -0
- package/lib/identifiers.d.ts +7 -0
- package/lib/identifiers.js +27 -0
- package/lib/index.d.ts +11 -4
- package/lib/index.js +7 -2
- package/lib/migrations.d.ts +8 -10
- package/lib/migrations.js +111 -18
- package/lib/query-builder.js +1 -1
- package/lib/tables.d.ts +197 -39
- package/package.json +14 -10
|
@@ -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
|
|
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 {
|
|
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';
|
package/lib/migrations.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
* changes nothing on that database and silently diverges
|
|
6
|
-
* code.
|
|
7
|
-
*
|
|
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`
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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>;
|