@kubun/db 0.7.0 → 0.8.1

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/db.d.ts CHANGED
@@ -1,122 +1,40 @@
1
- import { EventEmitter } from '@enkaku/event';
2
- import type { Adapter, AdapterTypes } from '@kubun/db-adapter';
3
- import { DocumentID, DocumentModelID } from '@kubun/id';
1
+ import type { Adapter, ColumnTypes, Functions } from '@kubun/db-adapter';
4
2
  import { type Logger } from '@kubun/logger';
5
- import type { DocumentModel, DocumentModelsRecord, DocumentNode } from '@kubun/protocol';
6
3
  import { Kysely, type Migration } from 'kysely';
7
- import type { CountDocumentsParams, Database, DocumentData, GraphModel, InsertDocumentAttachment, InsertMutationLogEntry, ListDocumentsParams, MutationLogEntry, QueryDocumentsParams, QueryDocumentsResult, SearchConfig, SearchDocumentResult, SearchDocumentsParams } from './types.js';
8
- export type Migrations = Record<string, Migration>;
9
- export type DocumentCreatedEvent = {
10
- type: 'create';
11
- document: DocumentNode;
12
- getCursor: () => string;
13
- };
14
- export type DocumentUpdatedEvent = {
15
- type: 'update';
16
- document: DocumentNode;
17
- previous: DocumentNode;
18
- getCursor: () => string;
19
- };
20
- export type DocumentSavedEvent = DocumentCreatedEvent | DocumentUpdatedEvent;
21
- export type DBEvents = {
22
- 'document:saved': DocumentSavedEvent;
23
- };
24
- export type DBParams<T extends AdapterTypes = AdapterTypes> = {
25
- adapter: Adapter<T>;
4
+ export type MigrationContext = {
5
+ types: ColumnTypes;
6
+ functions: Functions;
7
+ };
8
+ export type StoreDefinition<Tables, API> = {
9
+ name: string;
10
+ migrations: Record<string, Migration> | ((ctx: MigrationContext) => Record<string, Migration>);
11
+ dependsOn?: Array<string>;
12
+ createAPI: (db: Kysely<Tables>, adapter: Adapter) => API;
13
+ };
14
+ export type StoreProvider<Stores extends Record<string, unknown> = Record<string, unknown>> = {
15
+ getStore<S extends keyof Stores & string>(name: S): Promise<Stores[S]>;
16
+ onCommit(fn: () => void): void;
17
+ };
18
+ export type KubunDBParams = {
19
+ adapter: Adapter;
26
20
  logger?: Logger;
27
21
  };
28
- export type GraphModelWithRecord = GraphModel & {
29
- record: DocumentModelsRecord;
30
- };
31
- export type DocumentParams = {
32
- id: DocumentID;
33
- data: DocumentData | null;
34
- };
35
- export type CreateDocumentParams = DocumentParams & {
36
- owner: string;
37
- unique: Uint8Array;
38
- };
39
- export type SaveDocumentParams = DocumentParams & {
40
- existing: DocumentNode;
41
- };
42
- export type CreateGraphParams = {
43
- aliases?: Record<string, string>;
44
- id?: string;
45
- name?: string;
46
- record: DocumentModelsRecord;
47
- search?: SearchConfig;
48
- };
49
- export type AddDocumentModelParams = {
50
- id: string;
51
- graphModelID: string;
52
- model: DocumentModel;
53
- };
54
- export type WritableDB = {
55
- readonly events: EventEmitter<DBEvents>;
56
- getDocument(id: DocumentID): Promise<DocumentNode | null>;
57
- getDocumentModel(id: DocumentModelID | string): Promise<DocumentModel>;
58
- createDocument(params: CreateDocumentParams): Promise<DocumentNode>;
59
- saveDocument(params: SaveDocumentParams): Promise<DocumentNode>;
60
- insertMutationLogEntry(entry: InsertMutationLogEntry): Promise<void>;
61
- hasMutationHash(hash: string): Promise<boolean>;
62
- updateMutationStatus(mutationHash: string, status: string): Promise<void>;
63
- getFieldHLCs(documentID: string): Promise<Record<string, string> | null>;
64
- updateFieldHLCs(docID: DocumentID, fieldHLCs: Record<string, string>): Promise<void>;
65
- };
66
- export declare class KubunDB<T extends AdapterTypes = AdapterTypes> {
22
+ export declare class KubunDB implements StoreProvider {
67
23
  #private;
68
- constructor(params: DBParams<T>);
69
- /** @internal */
24
+ constructor(params: KubunDBParams);
70
25
  get adapter(): Adapter;
71
- get events(): EventEmitter<DBEvents>;
72
- getDB<T extends Database>(): Promise<Kysely<T>>;
73
- withTransaction<R>(fn: (db: WritableDB) => Promise<R>): Promise<R>;
74
- getMigrations(): Migrations;
26
+ register<Tables, API>(store: StoreDefinition<Tables, API>): void;
27
+ getStore<T>(name: string): Promise<T>;
28
+ onCommit(fn: () => void): void;
29
+ /**
30
+ * Runs `fn` inside a DB transaction and provides a scoped `StoreProvider`.
31
+ * Concurrent calls on the same `KubunDB` instance are supported — the
32
+ * underlying driver serializes them.
33
+ *
34
+ * Callers MUST use the provided `tx` inside the callback. Calling
35
+ * `db.withTransaction` on the same instance inside the callback opens a
36
+ * new independent transaction (no atomic nesting) and is a bug.
37
+ */
38
+ withTransaction<Stores extends Record<string, unknown>, R>(fn: (tx: StoreProvider<Stores>) => Promise<R>): Promise<R>;
75
39
  close(): Promise<void>;
76
- listGraphs(): Promise<Array<GraphModel>>;
77
- createGraph(params: CreateGraphParams): Promise<string>;
78
- getGraph(id: string): Promise<GraphModelWithRecord | null>;
79
- getDocumentModel(id: DocumentModelID | string): Promise<DocumentModel>;
80
- getDocument(id: DocumentID): Promise<DocumentNode | null>;
81
- listDocuments(params: ListDocumentsParams): Promise<Array<DocumentNode>>;
82
- countDocuments(params: CountDocumentsParams): Promise<number>;
83
- queryDocuments(params: QueryDocumentsParams): Promise<QueryDocumentsResult>;
84
- createDocument(params: CreateDocumentParams): Promise<DocumentNode>;
85
- saveDocument(params: SaveDocumentParams): Promise<DocumentNode>;
86
- insertMutationLogEntry(entry: InsertMutationLogEntry): Promise<void>;
87
- hasMutationHash(hash: string): Promise<boolean>;
88
- getMutationLogForDocuments(documentIDs: Array<string>): Promise<Array<MutationLogEntry>>;
89
- getPendingMutations(documentID: string): Promise<Array<MutationLogEntry>>;
90
- updateMutationStatus(mutationHash: string, status: string): Promise<void>;
91
- getFieldHLCs(documentID: string): Promise<Record<string, string> | null>;
92
- updateFieldHLCs(docID: DocumentID, fieldHLCs: Record<string, string>): Promise<void>;
93
- getDocumentIDsForScope(scopes: Array<{
94
- modelID: string;
95
- ownerDID: string;
96
- }>, excludedDocumentIDs?: Array<string>): Promise<Array<string>>;
97
- getDocumentMetadataForSync(modelID: string, documentIDs: Array<string>): Promise<Array<{
98
- id: string;
99
- model: string;
100
- owner: string;
101
- }>>;
102
- addAttachments(attachments: Array<InsertDocumentAttachment>): Promise<void>;
103
- getUserModelAccessDefault(ownerDID: string, modelID: string, permissionType: 'read' | 'write'): Promise<{
104
- level: string;
105
- allowedDIDs: Array<string> | null;
106
- } | null>;
107
- setUserModelAccessDefault(params: {
108
- ownerDID: string;
109
- modelID: string;
110
- permissionType: 'read' | 'write';
111
- accessLevel: string;
112
- allowedDIDs: Array<string> | null;
113
- }): Promise<void>;
114
- removeUserModelAccessDefaults(ownerDID: string, modelID: string, permissionTypes: Array<string>): Promise<void>;
115
- listDocumentModelIDs(): Promise<Array<string>>;
116
- queryDocumentsByOwner(modelID: string, ownerDID: string): Promise<Array<DocumentNode>>;
117
- createSearchIndex(modelID: string, fields: Array<string>): Promise<void>;
118
- dropSearchIndex(modelID: string): Promise<void>;
119
- updateSearchEntry(modelID: string, documentID: string, data: Record<string, unknown>, fields: Array<string>): Promise<void>;
120
- removeSearchEntry(modelID: string, documentID: string): Promise<void>;
121
- searchDocuments(params: SearchDocumentsParams): Promise<Array<SearchDocumentResult>>;
122
40
  }
package/lib/db.js CHANGED
@@ -1 +1 @@
1
- import{EventEmitter as e}from"@enkaku/event";import{DocumentID as t,DocumentModelID as a}from"@kubun/id";import{getKubunLogger as i}from"@kubun/logger";import{Kysely as r,Migrator as s,ParseJSONResultsPlugin as n}from"kysely";import{serializeCursor as d}from"./cursor.js";import{getMigrations as o}from"./migrations/migrations.js";import{applyDocumentFilter as l,applyDocumentOrderBy as u,applyPagination as c}from"./query-builder.js";function h(e){let t={version:e.version,name:e.name,behavior:e.behavior,interfaces:e.interfaces,schema:e.schema,fieldsMeta:e.fields_meta};return"unique"===t.behavior&&(t.uniqueFields=e.unique_fields),t}function m(e,t){return{id:t.id,model:t.model,owner:t.owner,data:t.data,createdAt:e.decodeTimestamp(t.created_at),updatedAt:t.updated_at?e.decodeTimestamp(t.updated_at):null}}function w(e,t){let i=a.fromString(t);return i.isLocal?i.toGlobal(a.fromString(e)).toString():t}export class KubunDB{#e;#t;#a;#i;#r;constructor(t){this.#e=t.adapter,this.#t=new r({dialect:this.#e.dialect,plugins:[new n]}),this.#a=new e,this.#i=t.logger??i("db"),this.#r=this.#s()}get adapter(){return this.#e}get events(){return this.#a}getDB(){return this.#r.then(()=>this.#t)}async withTransaction(e){await this.#r;let a=this.#e,i=this.#a,r=[],s=await this.#t.transaction().setIsolationLevel("read committed").execute(async s=>await e({events:i,getDocumentModel:async e=>h(await s.selectFrom("kubun_document_models").selectAll().where("id","=",e.toString()).executeTakeFirstOrThrow()),async getDocument(e){let t=await s.selectFrom(`k_${e.model.toString()}`).selectAll().where("id","=",e.toString()).executeTakeFirst();return t?m(a,t):null},async createDocument(e){let t=e.id.toString(),i=e.id.model.toString(),n=await s.insertInto(`k_${i}`).values({id:t,owner:e.owner,model:i,data:a.encodeJSON(e.data),unique:a.encodeBinary(e.unique)}).returningAll().executeTakeFirst(),o=m(a,n);return r.push({type:"create",document:o,getCursor:()=>d({id:n.id,ts:+n.created_at})}),o},async saveDocument(e){let t=e.id.toString(),i=await s.updateTable(`k_${e.id.model.toString()}`).set({data:e.data?a.encodeJSON(e.data):null,updated_at:a.encodeTimestamp(new Date)}).where("id","=",t).returningAll().executeTakeFirst(),n=m(a,i);return r.push({type:"update",document:n,previous:e.existing,getCursor:()=>d({id:i.id,ts:+i.created_at})}),n},async insertMutationLogEntry(e){await s.insertInto("kubun_mutation_log").values(e).onConflict(e=>e.doNothing()).execute()},hasMutationHash:async e=>null!=await s.selectFrom("kubun_mutation_log").select("mutation_hash").where("mutation_hash","=",e).executeTakeFirst(),async updateMutationStatus(e,t){await s.updateTable("kubun_mutation_log").set({status:t}).where("mutation_hash","=",e).execute()},async getFieldHLCs(e){let a=t.fromString(e).model.toString(),i=await s.selectFrom(`k_${a}`).select("field_hlcs").where("id","=",e).executeTakeFirst();if(i?.field_hlcs==null)return null;let r=i.field_hlcs;return"string"==typeof r?JSON.parse(r):r},async updateFieldHLCs(e,t){let i=e.model.toString();await s.updateTable(`k_${i}`).set({field_hlcs:a.encodeJSON(t)}).where("id","=",e.toString()).execute()}}));for(let e of r)i.emit("document:saved",e);return s}async #s(){let e=new s({db:this.#t,provider:{getMigrations:()=>Promise.resolve(this.getMigrations())}}),t=await e.migrateToLatest();if(null!=t.error)throw t.error;this.#i.info("ready")}getMigrations(){return o(this.#e)}async close(){await this.#r,await this.#t.destroy(),this.#i.info("closed")}async listGraphs(){return await this.#r,await this.#t.selectFrom("kubun_graph_models").selectAll().execute()}async createGraph(e){await this.#r;let t=e.id??globalThis.crypto.randomUUID(),a=e.name??t;return await this.#t.transaction().setIsolationLevel("read committed").execute(async i=>{await i.insertInto("kubun_graph_models").values({aliases:JSON.stringify(e.aliases??{}),id:t,name:a,search:e.search?JSON.stringify(e.search):null}).onConflict(e=>e.doNothing()).execute();let r=Object.entries(e.record),s=new Set;for(;s.size<r.length;){let e=r.filter(([e,t])=>!s.has(e)&&t.interfaces.every(t=>s.has(w(e,t))));if(0===e.length)throw Error("Could not create graph: no valid model to add");await Promise.all(e.map(async([e,a])=>{await this.#n(i,{id:e,graphModelID:t,model:a}),s.add(e)}))}}),this.#i.debug("created graph {id}",{id:t}),t}async #n(e,t){let{id:a,graphModelID:i,model:r}=t,s=this.#e.types;for(let t of(await this.#r,await e.schema.createTable(`k_${a}`).ifNotExists().addColumn("id",s.text,e=>e.notNull().primaryKey()).addColumn("owner",s.text,e=>e.notNull()).addColumn("model",s.text,e=>e.notNull()).addColumn("data",s.json).addColumn("field_hlcs",s.json).addColumn("unique",s.binary,e=>e.notNull()).addColumn("created_at",s.timestamp,e=>e.defaultTo(this.#e.functions.now).notNull()).addColumn("updated_at",s.timestamp).execute(),await e.insertInto("kubun_document_models").values({id:a,version:r.version,name:r.name,behavior:r.behavior,unique_fields:"unique"===r.behavior?JSON.stringify(r.uniqueFields):null,interfaces:JSON.stringify(r.interfaces),schema:JSON.stringify(r.schema),fields_meta:JSON.stringify(r.fieldsMeta)}).onConflict(e=>e.doNothing()).execute(),r.interfaces))await e.insertInto("kubun_document_model_interfaces").values({interface_id:w(a,t),implementation_id:a}).onConflict(e=>e.doNothing()).execute();await e.insertInto("kubun_graph_document_models").values({graph_model_id:i,document_model_id:a}).onConflict(e=>e.doNothing()).execute()}async getGraph(e){await this.#r;let t=await this.#t.selectFrom("kubun_graph_models").selectAll().where("id","=",e).executeTakeFirst();if(null==t)return null;let a={};for(let t of(await this.#t.selectFrom("kubun_graph_document_models").innerJoin("kubun_document_models","kubun_graph_document_models.document_model_id","kubun_document_models.id").selectAll("kubun_document_models").where("kubun_graph_document_models.graph_model_id","=",e).execute()))a[t.id]=h(t);return{...t,record:a}}async getDocumentModel(e){return await this.#r,h(await this.#t.selectFrom("kubun_document_models").selectAll().where("id","=",e.toString()).executeTakeFirstOrThrow())}async getDocument(e){await this.#r;let t=await this.#t.selectFrom(`k_${e.model.toString()}`).selectAll().where("id","=",e.toString()).executeTakeFirst();return t?m(this.#e,t):null}async listDocuments(e){await this.#r;let[t,...a]=e.modelIDs,i=this.#t.selectFrom(`k_${t}`).selectAll().where("id","in",e.docIDs);for(let t of a)i=i.unionAll(a=>a.selectFrom(`k_${t}`).selectAll().where("id","in",e.docIDs));return(await i.execute()).map(e=>m(this.#e,e))}async countDocuments(e){let{filter:t,modelIDs:a,owner:i}=e;await this.#r;let r=this.#e.coerceFilterValue.bind(this.#e);return(await Promise.all(a.map(async e=>{let a=this.#t.selectFrom(`k_${e}`).select(e=>e.fn.countAll().as("count"));return null!=i&&(a=a.where("owner","=",i)),null!=t&&(a=a.where(e=>l(e,t,[],r))),Number((await a.executeTakeFirstOrThrow()).count)}))).reduce((e,t)=>e+t,0)}async queryDocuments(e){let{filter:t,modelIDs:a,orderBy:i,owner:r,...s}=e,n=null!=e.last;await this.#r;let o=this.#e.coerceFilterValue.bind(this.#e),[h,...w]=a,_=this.#t.selectFrom(`k_${h}`).selectAll();for(let e of(null!=r&&(_=_.where("owner","=",r)),null!=t&&(_=_.where(e=>l(e,t,[],o))),w))_=_.unionAll(a=>{let i=a.selectFrom(`k_${e}`).selectAll();return null!=r&&(i=i.where("owner","=",r)),null!=t&&(i=i.where(e=>l(e,t,[],o))),i});let[p,y]=u(_,i,n),[g,f]=c(p,s,i),b=await g.execute(),k=n?b.slice(0,f).reverse():b.slice(0,f);return{entries:y?k.map(e=>{let t={};for(let a of y){let i=e.data;for(let e of a)null!=i&&(i=i[e]);null!=i&&(t[a.join(".")]=i)}return{cursor:d({id:e.id,values:t}),document:m(this.#e,e)}}):k.map(e=>({cursor:d({id:e.id,ts:+e.created_at}),document:m(this.#e,e)})),hasMore:k.length>f}}async createDocument(e){await this.#r;let t=e.id.toString(),a=e.id.model.toString(),i={id:t,owner:e.owner,model:a,data:this.#e.encodeJSON(e.data),unique:this.#e.encodeBinary(e.unique)},r=await this.#t.transaction().setIsolationLevel("read committed").execute(async e=>await e.insertInto(`k_${a}`).values(i).returningAll().executeTakeFirst()),s=m(this.#e,r);return this.#a.emit("document:saved",{type:"create",document:s,getCursor:()=>d({id:r.id,ts:+r.created_at})}),this.#i.debug("created document {id} with model {model}",{id:t,model:a}),s}async saveDocument(e){await this.#r;let t=e.id.toString(),a={data:e.data?this.#e.encodeJSON(e.data):null,updated_at:this.#e.encodeTimestamp(new Date)},i=await this.#t.transaction().setIsolationLevel("read committed").execute(async i=>await i.updateTable(`k_${e.id.model.toString()}`).set(a).where("id","=",t).returningAll().executeTakeFirst()),r=m(this.#e,i);return this.#a.emit("document:saved",{type:"update",document:r,previous:e.existing,getCursor:()=>d({id:i.id,ts:+i.created_at})}),this.#i.debug("updated document {id}",{id:t}),r}async insertMutationLogEntry(e){await this.#r,await this.#t.insertInto("kubun_mutation_log").values(e).onConflict(e=>e.doNothing()).execute()}async hasMutationHash(e){return await this.#r,null!=await this.#t.selectFrom("kubun_mutation_log").select("mutation_hash").where("mutation_hash","=",e).executeTakeFirst()}async getMutationLogForDocuments(e){return 0===e.length?[]:(await this.#r,await this.#t.selectFrom("kubun_mutation_log").selectAll().where("document_id","in",e).orderBy("hlc","asc").execute())}async getPendingMutations(e){return await this.#r,await this.#t.selectFrom("kubun_mutation_log").selectAll().where("document_id","=",e).where("status","=","pending").orderBy("hlc","asc").execute()}async updateMutationStatus(e,t){await this.#r,await this.#t.updateTable("kubun_mutation_log").set({status:t}).where("mutation_hash","=",e).execute()}async getFieldHLCs(e){await this.#r;let a=t.fromString(e).model.toString(),i=await this.#t.selectFrom(`k_${a}`).select("field_hlcs").where("id","=",e).executeTakeFirst();if(i?.field_hlcs==null)return null;let r=i.field_hlcs;return"string"==typeof r?JSON.parse(r):r}async updateFieldHLCs(e,t){await this.#r;let a=e.model.toString();await this.#t.updateTable(`k_${a}`).set({field_hlcs:this.#e.encodeJSON(t)}).where("id","=",e.toString()).execute()}async getDocumentIDsForScope(e,t=[]){await this.#r;let a=[];for(let t of e)try{let e=await this.#t.selectFrom(`k_${t.modelID}`).select("id").where("owner","=",t.ownerDID).execute();a.push(...e.map(e=>e.id))}catch{}if(t.length>0){let e=new Set(t);return a.filter(t=>!e.has(t))}return a}async getDocumentMetadataForSync(e,t){return 0===t.length?[]:(await this.#r,(await this.#t.selectFrom(`k_${e}`).select(["id","model","owner"]).where("id","in",t).execute()).map(e=>({id:e.id,model:e.model,owner:e.owner})))}async addAttachments(e){await this.#r,await this.#t.insertInto("kubun_document_attachments").values(e).onConflict(e=>e.doNothing()).execute()}async getUserModelAccessDefault(e,t,a){await this.#r;let i=await this.#t.selectFrom("kubun_user_model_access_defaults").select(["access_level","allowed_dids"]).where("owner_did","=",e).where("model_id","=",t).where("permission_type","=",a).executeTakeFirst();return i?{level:i.access_level,allowedDIDs:i.allowed_dids}:null}async setUserModelAccessDefault(e){await this.#r,await this.#t.insertInto("kubun_user_model_access_defaults").values({owner_did:e.ownerDID,model_id:e.modelID,permission_type:e.permissionType,access_level:e.accessLevel,allowed_dids:e.allowedDIDs?this.#e.encodeJSON(e.allowedDIDs):null}).onConflict(t=>t.columns(["owner_did","model_id","permission_type"]).doUpdateSet({access_level:e.accessLevel,allowed_dids:e.allowedDIDs?this.#e.encodeJSON(e.allowedDIDs):null,updated_at:this.#e.encodeTimestamp(new Date)})).execute()}async removeUserModelAccessDefaults(e,t,a){await this.#r,await this.#t.deleteFrom("kubun_user_model_access_defaults").where("owner_did","=",e).where("model_id","=",t).where("permission_type","in",a).execute()}async listDocumentModelIDs(){return await this.#r,(await this.#t.selectFrom("kubun_document_models").select("id").execute()).map(e=>e.id)}async queryDocumentsByOwner(e,t){return await this.#r,(await this.#t.selectFrom(`k_${e}`).selectAll().where("owner","=",t).execute()).map(e=>m(this.#e,e))}async createSearchIndex(e,t){await this.#r,await this.#e.createSearchIndex(this.#t,{modelID:e,fields:t})}async dropSearchIndex(e){await this.#r,await this.#e.dropSearchIndex(this.#t,e)}async updateSearchEntry(e,t,a,i){await this.#r;let r={};for(let e of i){let t=this.#d(a,e);null!=t&&(r[e]=String(t))}await this.#e.updateSearchEntry(this.#t,e,t,r,i)}async removeSearchEntry(e,t){await this.#r,await this.#e.removeSearchEntry(this.#t,e,t)}async searchDocuments(e){await this.#r;let t=[];for(let a of e.modelIDs)for(let i of(await this.#e.searchIndex(this.#t,{query:e.query,modelID:a,limit:e.first??50})))t.push({documentID:i.documentID,modelID:a,rank:i.rank});return(t.sort((e,t)=>Math.abs(t.rank)-Math.abs(e.rank)),e.first)?t.slice(0,e.first):t}#d(e,t){let a=t.split("."),i=e;for(let e of a){if(null==i||"object"!=typeof i)return null;i=i[e]}return i}}
1
+ import{getKubunLogger as t}from"@kubun/logger";import{Kysely as e,Migrator as r,ParseJSONResultsPlugin as i}from"kysely";export class KubunDB{#t;#e;#r;#i=new Map;#s=new Map;#a=new Map;constructor(r){this.#t=r.adapter,this.#e=new e({dialect:this.#t.dialect,plugins:[new i]}),this.#r=r.logger??t("db")}get adapter(){return this.#t}register(t){let e=this.#a.get(t.name);if(null!=e){e!==t&&this.#r.warn('store "{name}" registered with different definition, ignoring',{name:t.name});return}this.#a.set(t.name,t)}async getStore(t){if(await this.#o(t),!this.#s.has(t)){let e=this.#a.get(t);if(null==e)throw Error(`Store "${t}" is not registered`);this.#s.set(t,e.createAPI(this.#e,this.#t))}return this.#s.get(t)}onCommit(t){t()}async withTransaction(t){await Promise.all(Array.from(this.#a.keys()).map(t=>this.#o(t)));let e=[],r=await this.#e.transaction().execute(async r=>{let i=new Map;return await t({getStore:async t=>{if(!i.has(t)){let e=this.#a.get(t);if(null==e)throw Error(`Store "${t}" is not registered`);i.set(t,e.createAPI(r,this.#t))}return i.get(t)},onCommit:t=>{e.push(t)}})});for(let t of e)t();return r}async close(){await this.#e.destroy(),this.#r.info("closed")}async #o(t){let e=this.#i.get(t);return null==e&&(e=this.#n(t).catch(e=>{throw this.#i.delete(t),e}),this.#i.set(t,e)),e}async #n(t){let e=this.#a.get(t);if(null==e)throw Error(`Store "${t}" is not registered`);for(let t of e.dependsOn??[])await this.#o(t);let i="function"==typeof e.migrations?e.migrations({types:this.#t.types,functions:this.#t.functions}):e.migrations;if(0===Object.keys(i).length)return;let s=new r({db:this.#e,provider:{getMigrations:()=>Promise.resolve(i)},migrationTableName:`kubun_${t}_migration`,migrationLockTableName:`kubun_${t}_migration_lock`}),a=await s.migrateToLatest();if(null!=a.error)throw a.error;this.#r.info("store {name} migrations complete",{name:t})}}
package/lib/index.d.ts CHANGED
@@ -1,3 +1 @@
1
- export type { CreateGraphParams, DBEvents, DBParams, DocumentCreatedEvent, DocumentSavedEvent, GraphModelWithRecord, Migrations, WritableDB, } from './db.js';
2
- export { KubunDB } from './db.js';
3
- export type * from './types.js';
1
+ export { KubunDB, type KubunDBParams, type MigrationContext, type StoreDefinition, type StoreProvider, } from './db.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/db",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "license": "see LICENSE.md",
5
5
  "keywords": [],
6
6
  "type": "module",
@@ -15,18 +15,12 @@
15
15
  ],
16
16
  "sideEffects": false,
17
17
  "dependencies": {
18
- "@enkaku/codec": "^0.14.0",
19
- "@enkaku/event": "^0.14.1",
20
- "kysely": "^0.28.11",
21
- "@kubun/logger": "^0.7.0"
18
+ "kysely": "^0.28.16",
19
+ "@kubun/db-adapter": "^0.8.1",
20
+ "@kubun/logger": "^0.8.0"
22
21
  },
23
22
  "devDependencies": {
24
- "@testcontainers/postgresql": "^11.12.0",
25
- "@kubun/db-better-sqlite": "^0.7.0",
26
- "@kubun/db-postgres": "^0.7.0",
27
- "@kubun/db-adapter": "^0.7.0",
28
- "@kubun/protocol": "^0.7.0",
29
- "@kubun/id": "^0.7.0"
23
+ "@kubun/db-better-sqlite": "^0.8.1"
30
24
  },
31
25
  "scripts": {
32
26
  "build:clean": "del lib",
@@ -34,9 +28,8 @@
34
28
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
35
29
  "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
36
30
  "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
37
- "test:types": "tsc --noEmit --skipLibCheck",
38
- "test:vitest": "vitest run",
39
- "test:unit": "pnpm rebuild && pnpm run test:vitest",
31
+ "test:types": "tsc --noEmit -p tsconfig.test.json",
32
+ "test:unit": "vitest run",
40
33
  "test": "pnpm run test:types && pnpm run test:unit"
41
34
  }
42
35
  }
package/lib/cursor.d.ts DELETED
@@ -1,7 +0,0 @@
1
- export type CursorData = {
2
- id: string;
3
- ts?: number;
4
- values?: Record<string, unknown>;
5
- };
6
- export declare function parseCursor(cursor: string): CursorData;
7
- export declare function serializeCursor(data: CursorData): string;
package/lib/cursor.js DELETED
@@ -1 +0,0 @@
1
- import{b64uFromJSON as r,b64uToJSON as e}from"@enkaku/codec";export function parseCursor(r){return e(r)}export function serializeCursor(e){return r(e)}
@@ -1,3 +0,0 @@
1
- import type { Adapter } from '@kubun/db-adapter';
2
- import type { Migration } from 'kysely';
3
- export declare function getMigration(adapter: Adapter): Migration;
@@ -1 +0,0 @@
1
- export function getMigration(e){let t=e.types;return{up:async function(a){await a.schema.createTable("kubun_document_attachments").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("data",t.binary,e=>e.notNull()).addColumn("created_at",t.timestamp,t=>t.defaultTo(e.functions.now).notNull()).execute(),await a.schema.createTable("kubun_document_models").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("version",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("behavior",t.text,e=>e.notNull()).addColumn("unique_fields",t.json).addColumn("interfaces",t.json,e=>e.notNull()).addColumn("schema",t.json,e=>e.notNull()).addColumn("fields_meta",t.json,e=>e.notNull()).addColumn("created_at",t.timestamp,t=>t.defaultTo(e.functions.now).notNull()).execute(),await a.schema.createTable("kubun_document_model_interfaces").ifNotExists().addColumn("interface_id",t.text,e=>e.notNull().references("kubun_document_models.id")).addColumn("implementation_id",t.text,e=>e.notNull().references("kubun_document_models.id")).addUniqueConstraint("kubun_document_model_interfaces_pkey",["interface_id","implementation_id"]).execute(),await a.schema.createTable("kubun_graph_models").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("name",t.text,e=>e.notNull()).addColumn("aliases",t.json,e=>e.defaultTo("{}").notNull()).addColumn("search",t.json).addColumn("created_at",t.timestamp,t=>t.defaultTo(e.functions.now).notNull()).addColumn("updated_at",t.timestamp).execute(),await a.schema.createTable("kubun_graph_document_models").ifNotExists().addColumn("graph_model_id",t.text,e=>e.notNull().references("kubun_graph_models.id")).addColumn("document_model_id",t.text,e=>e.notNull().references("kubun_document_models.id")).addUniqueConstraint("kubun_graph_document_models_pkey",["graph_model_id","document_model_id"]).execute(),await a.schema.createTable("kubun_user_model_access_defaults").ifNotExists().addColumn("owner_did",t.text,e=>e.notNull()).addColumn("model_id",t.text,e=>e.notNull()).addColumn("permission_type",t.text,e=>e.notNull()).addColumn("access_level",t.text,e=>e.notNull()).addColumn("allowed_dids",t.json).addColumn("created_at",t.timestamp,t=>t.defaultTo(e.functions.now).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("kubun_user_model_access_defaults_pkey",["owner_did","model_id","permission_type"]).execute(),await a.schema.createIndex("idx_user_model_access_owner_model").on("kubun_user_model_access_defaults").columns(["owner_did","model_id"]).execute(),await a.schema.createTable("kubun_sync_checkpoints").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_server_id",t.text,e=>e.notNull()).addColumn("user_id",t.text,e=>e.notNull()).addColumn("last_sync_timestamp",t.timestamp,e=>e.notNull()).addColumn("document_checkpoints",t.json,e=>e.notNull()).addColumn("vector_clock",t.json,e=>e.notNull()).execute(),await a.schema.createIndex("idx_sync_checkpoints_peer_user").on("kubun_sync_checkpoints").columns(["peer_server_id","user_id"]).execute(),await a.schema.createTable("kubun_sync_peers").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull().unique()).addColumn("endpoint",t.text,e=>e.notNull()).addColumn("mode",t.text,e=>e.notNull()).addColumn("allowed_users",t.json,e=>e.notNull()).addColumn("priority",t.text,e=>e.notNull().defaultTo("0")).addColumn("trust_level",t.text,e=>e.notNull()).addColumn("config",t.json,e=>e.notNull()).addColumn("created_at",t.timestamp,t=>t.defaultTo(e.functions.now).notNull()).addColumn("updated_at",t.timestamp,t=>t.defaultTo(e.functions.now).notNull()).execute(),await a.schema.createTable("kubun_mutation_log").ifNotExists().addColumn("mutation_hash",t.text,e=>e.primaryKey()).addColumn("model_id",t.text,e=>e.notNull()).addColumn("document_id",t.text,e=>e.notNull()).addColumn("author_did",t.text,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("mutation_jwt",t.text,e=>e.notNull()).addColumn("status",t.text,e=>e.notNull().defaultTo("applied")).execute(),await a.schema.createIndex("idx_mutation_log_hlc").on("kubun_mutation_log").column("hlc").execute(),await a.schema.createIndex("idx_mutation_log_document").on("kubun_mutation_log").column("document_id").execute(),await a.schema.createIndex("idx_mutation_log_status").on("kubun_mutation_log").column("status").execute()},down:async function(e){await e.schema.dropTable("kubun_mutation_log").ifExists().execute(),await e.schema.dropTable("kubun_sync_peers").ifExists().execute(),await e.schema.dropTable("kubun_sync_checkpoints").ifExists().execute(),await e.schema.dropTable("kubun_user_model_access_defaults").ifExists().execute(),await e.schema.dropTable("kubun_graph_document_models").ifExists().execute(),await e.schema.dropTable("kubun_graph_models").ifExists().execute(),await e.schema.dropTable("kubun_document_model_interfaces").ifExists().execute(),await e.schema.dropTable("kubun_document_models").ifExists().execute(),await e.schema.dropTable("kubun_document_attachments").ifExists().execute()}}}
@@ -1,3 +0,0 @@
1
- import type { Adapter } from '@kubun/db-adapter';
2
- import type { Migration } from 'kysely';
3
- export declare function getMigrations(adapter: Adapter): Record<string, Migration>;
@@ -1 +0,0 @@
1
- import{getMigration as i}from"./0-init.js";export function getMigrations(t){return{kubunMigration0:i(t)}}
@@ -1,12 +0,0 @@
1
- import type { AnyValueFilter, DocumentFilter, DocumentOrderBy } from '@kubun/protocol';
2
- import type { ExpressionBuilder, ExpressionWrapper, SelectQueryBuilder } from 'kysely';
3
- import type { ConnectionArguments, Database, Document } from './types.js';
4
- export type CoerceFilterValue = (value: unknown) => unknown;
5
- type DocumentExpressionBuilder = ExpressionBuilder<Database, 'k_document'>;
6
- type DocumentExpressionWrapper = ExpressionWrapper<Database, 'k_document', any>;
7
- export type DocumentQueryBuilder = SelectQueryBuilder<Database, 'k_document', Document>;
8
- export declare function applyPagination(query: DocumentQueryBuilder, args: ConnectionArguments, orderBy?: DocumentOrderBy): [DocumentQueryBuilder, number];
9
- export declare function applyDocumentFilter(eb: DocumentExpressionBuilder, filter: DocumentFilter, path?: Array<string>, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
10
- export declare function applyValueFilter(eb: DocumentExpressionBuilder, keys: Array<string>, filter: AnyValueFilter, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
11
- export declare function applyDocumentOrderBy(queryBuilder: DocumentQueryBuilder, orderBy?: DocumentOrderBy, isReverse?: boolean): [DocumentQueryBuilder, Array<Array<string>> | null];
12
- export {};
@@ -1 +0,0 @@
1
- import{parseCursor as e}from"./cursor.js";function t(e=50){return Math.min(e,100)}function r(e,t){let r=e.ref("data","->>");for(let e of t)r=r.key(e);return r}function n(e){let t=[],r=e;do{let[e,n]=Object.entries(r)[0];if(t.push(e),"string"==typeof n)return{keys:t,direction:n};r=n}while(null!=r)throw Error("Could not extract field entry")}function l(e,t=!1){return t?"asc"===e?"desc":"asc":e}export function applyPagination(l,a,o=[]){let{first:i,last:u,before:c,after:d}=a;if(null!=i){let a=t(i);return[function(t,l,a,o){let i=t;if(null!=a){let{id:t,ts:l,values:u}=e(a);null!=l?i=i.where(e=>e.or([e("created_at",">",l),e("created_at","=",l).and("id",">",t)])):null!=u&&(i=i.where(e=>{let l=[];for(let a of o){let o=n(a),i=u[o.keys.join(".")];if(null==i)continue;let c=r(e,o.keys);l.push(e.or([e(c,"asc"===o.direction?">":"<",i),e(c,"=",i).and("id",">",t)]))}return e.and(l)}))}return i.orderBy("created_at","asc").limit(l)}(l,a+1,d,o),a]}if(null!=u){let a=t(u);return[function(t,l,a,o){let i=t;if(null!=a){let{id:t,ts:l,values:u}=e(a);null!=l?i=i.where(e=>e.or([e("created_at","<",l),e("created_at","=",l).and("id","<",t)])):null!=u&&(i=i.where(e=>{let l=[];for(let a of o){let o=n(a),i=u[o.keys.join(".")];if(null==i)continue;let c=r(e,o.keys);l.push(e.or([e(c,"asc"===o.direction?"<":">",i),e(c,"=",i).and("id",">",t)]))}return e.and(l)}))}return i.orderBy("created_at","desc").limit(l)}(l,a+1,c,o),a]}let s=t();return[l.orderBy("created_at","asc").limit(s+1),s]}export function applyDocumentFilter(e,t,r=[],n){let l=Object.entries(t);if(1!==l.length)throw Error("Invalid document filter");let[a,o]=l[0];switch(a){case"where":var i,u,c,d;let s;return i=e,u=o,c=r,d=n,s=Object.entries(u).map(([e,t])=>applyValueFilter(i,[...c,e],t,d)),i.and(s);case"and":return e.and(o.map(t=>applyDocumentFilter(e,t,r,n)));case"or":return e.or(o.map(t=>applyDocumentFilter(e,t,r,n)));case"not":return e.not(applyDocumentFilter(e,o,r,n));default:throw Error(`Invalid document filter type: ${a}`)}}export function applyValueFilter(e,t,n,l){let a=Object.entries(n);if(1!==a.length)throw Error("Invalid value filter");let o=r(e,t),[i,u]=a[0],c=l??(e=>e);switch(i){case"isNull":return e(o,!0===u?"is":"is not",null);case"equalTo":return e(o,"=",c(u));case"notEqualTo":return e(o,"!=",c(u));case"in":return e(o,"in",u.map(c));case"notIn":return e(o,"not in",u.map(c));case"lessThan":return e(o,"<",c(u));case"lessThanOrEqualTo":return e(o,"<=",c(u));case"greaterThan":return e(o,">",c(u));case"greaterThanOrEqualTo":return e(o,">=",c(u));default:throw Error(`Invalid value filter type: ${i}`)}}export function applyDocumentOrderBy(e,t=[],n=!1){if(0===t.length)return[e.orderBy("created_at",l("asc",n)),null];let o=e,i=[];for(let e of t){let[t,u]=function e(t,n,o,i=[]){let u=Object.entries(n);if(1!==u.length)throw Error("Invalid order by field");let[c,d]=u[0],s=[...i,c],f=a[c];return null!=f?[t.orderBy(f,l(d,o)),s]:"string"==typeof d?[t.orderBy(e=>r(e,s),l(d,o)),s]:e(t,d,o,s)}(o,e,n);o=t,i.push(u)}return[o,i]}let a={_createdAt:"created_at",_owner:"owner"};
package/lib/types.d.ts DELETED
@@ -1,184 +0,0 @@
1
- import type { DocumentFieldsMeta, DocumentFilter, DocumentModelSchema, DocumentNode, DocumentOrderBy } from '@kubun/protocol';
2
- import type { ColumnType, Insertable, JSONColumnType, Selectable, Updateable } from 'kysely';
3
- type CreatedAtColumn = ColumnType<number, number | undefined, never>;
4
- type UpdatedAtColumn = ColumnType<number, number | undefined, number | undefined>;
5
- export type ConnectionArguments = {
6
- before?: string | null;
7
- after?: string | null;
8
- first?: number | null;
9
- last?: number | null;
10
- };
11
- export type DocumentData = Record<string, unknown>;
12
- export type DocumentTable<Data extends DocumentData = DocumentData> = {
13
- id: string;
14
- owner: string;
15
- model: string;
16
- data: JSONColumnType<Data> | null;
17
- field_hlcs: JSONColumnType<Record<string, string>> | null;
18
- unique: Uint8Array;
19
- created_at: CreatedAtColumn;
20
- updated_at: UpdatedAtColumn;
21
- };
22
- export type Document<Data extends DocumentData = DocumentData> = Selectable<DocumentTable<Data>>;
23
- export type InsertDocument<Data extends DocumentData = DocumentData> = Insertable<DocumentTable<Data>>;
24
- export type UpdateDocument<Data extends DocumentData = DocumentData> = Updateable<DocumentTable<Data>>;
25
- export type DocumentAttachmentTable = {
26
- id: string;
27
- data: Uint8Array;
28
- created_at: CreatedAtColumn;
29
- };
30
- export type DocumentAttachment = Selectable<DocumentAttachmentTable>;
31
- export type InsertDocumentAttachment = Insertable<DocumentAttachmentTable>;
32
- export type DocumentModelTable = {
33
- id: string;
34
- version: string;
35
- name: string;
36
- behavior: string;
37
- unique_fields: JSONColumnType<Array<string>> | null;
38
- interfaces: JSONColumnType<Array<string>>;
39
- schema: JSONColumnType<DocumentModelSchema>;
40
- fields_meta: JSONColumnType<DocumentFieldsMeta>;
41
- created_at: CreatedAtColumn;
42
- updated_at: UpdatedAtColumn;
43
- };
44
- export type DocumentModel = Selectable<DocumentModelTable>;
45
- export type InsertDocumentModel = Insertable<DocumentModelTable>;
46
- export type UpdateDocumentModel = Updateable<DocumentModelTable>;
47
- export type DocumentModelInterfaceTable = {
48
- interface_id: string;
49
- implementation_id: string;
50
- };
51
- export type DocumentModelInterface = Selectable<DocumentModelInterfaceTable>;
52
- export type InsertDocumentModelInterface = Insertable<DocumentModelInterfaceTable>;
53
- export type MutationLogTable = {
54
- mutation_hash: string;
55
- model_id: string;
56
- document_id: string;
57
- author_did: string;
58
- hlc: string;
59
- mutation_jwt: string;
60
- status: string;
61
- };
62
- export type MutationLogEntry = Selectable<MutationLogTable>;
63
- export type InsertMutationLogEntry = Insertable<MutationLogTable>;
64
- export type SearchConfig = Record<string, {
65
- fields?: Array<string>;
66
- }>;
67
- export type SearchDocumentsParams = {
68
- query: string;
69
- modelIDs: Array<string>;
70
- first?: number;
71
- after?: string;
72
- };
73
- export type SearchDocumentResult = {
74
- documentID: string;
75
- modelID: string;
76
- rank: number;
77
- };
78
- export type GraphModelTable = {
79
- id: string;
80
- name: string;
81
- aliases: JSONColumnType<Record<string, string>>;
82
- search: JSONColumnType<SearchConfig> | null;
83
- created_at: CreatedAtColumn;
84
- updated_at: UpdatedAtColumn;
85
- };
86
- export type GraphModel = Selectable<GraphModelTable>;
87
- export type InsertGraphModel = Insertable<GraphModelTable>;
88
- export type UpdateGraphModel = Updateable<GraphModelTable>;
89
- export type GraphDocumentModelTable = {
90
- graph_model_id: string;
91
- document_model_id: string;
92
- };
93
- export type GraphDocumentModel = Selectable<GraphDocumentModelTable>;
94
- export type InsertGraphDocumentModel = Insertable<GraphDocumentModelTable>;
95
- export type UserModelAccessDefaultTable = {
96
- owner_did: string;
97
- model_id: string;
98
- permission_type: string;
99
- access_level: string;
100
- allowed_dids: JSONColumnType<Array<string>> | null;
101
- created_at: CreatedAtColumn;
102
- updated_at: UpdatedAtColumn;
103
- };
104
- export type UserModelAccessDefault = Selectable<UserModelAccessDefaultTable>;
105
- export type InsertUserModelAccessDefault = Insertable<UserModelAccessDefaultTable>;
106
- export type UpdateUserModelAccessDefault = Updateable<UserModelAccessDefaultTable>;
107
- export type SyncCheckpointTable = {
108
- id: string;
109
- peer_server_id: string;
110
- user_id: string;
111
- last_sync_timestamp: ColumnType<number, number, number>;
112
- document_checkpoints: JSONColumnType<Record<string, {
113
- lastKnownHash: string;
114
- lastSyncedSequence: number;
115
- }>>;
116
- vector_clock: JSONColumnType<Record<string, number>>;
117
- };
118
- export type SyncCheckpoint = Selectable<SyncCheckpointTable>;
119
- export type InsertSyncCheckpoint = Insertable<SyncCheckpointTable>;
120
- export type SyncPeerTable = {
121
- id: string;
122
- peer_did: string;
123
- endpoint: string;
124
- mode: string;
125
- allowed_users: JSONColumnType<Array<string> | {
126
- all: boolean;
127
- }>;
128
- priority: string;
129
- trust_level: string;
130
- config: JSONColumnType<Record<string, unknown>>;
131
- created_at: CreatedAtColumn;
132
- updated_at: UpdatedAtColumn;
133
- };
134
- export type SyncPeer = Selectable<SyncPeerTable>;
135
- export type InsertSyncPeer = Insertable<SyncPeerTable>;
136
- export type AccessPermissions = {
137
- read?: {
138
- level: 'only_owner' | 'anyone' | 'allowed_dids';
139
- allowedDIDs?: Array<string>;
140
- };
141
- write?: {
142
- level: 'only_owner' | 'allowed_dids';
143
- allowedDIDs?: Array<string>;
144
- };
145
- };
146
- export type DocumentTables<Models extends Record<string, DocumentData> = Record<string, DocumentData>, Keys = keyof Models> = {
147
- [K in string & Keys]: DocumentTable<Models[K]>;
148
- };
149
- export type Database = {
150
- kubun_document_attachments: DocumentAttachmentTable;
151
- kubun_document_models: DocumentModelTable;
152
- kubun_document_model_interfaces: DocumentModelInterfaceTable;
153
- kubun_mutation_log: MutationLogTable;
154
- kubun_graph_document_models: GraphDocumentModelTable;
155
- kubun_graph_models: GraphModelTable;
156
- kubun_user_model_access_defaults: UserModelAccessDefaultTable;
157
- kubun_sync_checkpoints: SyncCheckpointTable;
158
- kubun_sync_peers: SyncPeerTable;
159
- [key: `k_${string}`]: DocumentTable;
160
- };
161
- export type CursorDocument = {
162
- cursor: string;
163
- document: DocumentNode;
164
- };
165
- export type ListDocumentsParams = {
166
- modelIDs: Array<string>;
167
- docIDs: Array<string>;
168
- };
169
- export type QueryDocumentsParams = ConnectionArguments & {
170
- modelIDs: Array<string>;
171
- filter?: DocumentFilter;
172
- orderBy?: DocumentOrderBy;
173
- owner?: string;
174
- };
175
- export type CountDocumentsParams = {
176
- modelIDs: Array<string>;
177
- filter?: DocumentFilter;
178
- owner?: string;
179
- };
180
- export type QueryDocumentsResult = {
181
- entries: Array<CursorDocument>;
182
- hasMore: boolean;
183
- };
184
- export {};
package/lib/types.js DELETED
@@ -1 +0,0 @@
1
- export{};