@kubun/mutation 0.8.0 → 0.9.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/apply.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { type Validator } from '@enkaku/schema';
2
- import type { WritableDB } from '@kubun/db';
3
2
  import { HLC } from '@kubun/hlc';
4
- import type { ChangeDocumentMutation, DocumentData, DocumentMutation, DocumentNode, SetDocumentMutation } from '@kubun/protocol';
3
+ import type { DocumentModelID } from '@kubun/id';
4
+ import { DocumentID } from '@kubun/id';
5
+ import type { ChangeDocumentMutation, DocumentData, DocumentModel, DocumentMutation, DocumentNode, SetDocumentMutation } from '@kubun/protocol';
5
6
  export type DocumentValidator = Validator<DocumentData>;
6
7
  export type ValidatorsRecord = Record<string, Promise<DocumentValidator>>;
7
8
  export type AccessChecker = (doc: {
@@ -10,15 +11,52 @@ export type AccessChecker = (doc: {
10
11
  owner: string;
11
12
  data: DocumentData | null;
12
13
  }, permissionType: 'read' | 'write') => Promise<boolean>;
14
+ /**
15
+ * Post-state access gate evaluated AFTER the post-apply `DocumentNode` is
16
+ * computed in memory but BEFORE any persist call. Returning `false` instructs
17
+ * the apply functions to skip persistence and return `null`. Persistence-side
18
+ * effects (`saveDocument`, `createDocument`, `updateFieldHLCs`) are not
19
+ * invoked when the gate denies, so engine state is unchanged.
20
+ */
21
+ export type PostStateGate = (postState: DocumentNode) => Promise<boolean> | boolean;
22
+ /**
23
+ * Minimal database interface required by mutation operations.
24
+ * This decouples @kubun/mutation from the full KubunDB/store API,
25
+ * allowing it to work with any implementation that provides these methods.
26
+ */
27
+ export type MutationDB = {
28
+ getDocument(id: DocumentID): Promise<DocumentNode | null>;
29
+ getDocumentModel(id: DocumentModelID | string): Promise<DocumentModel>;
30
+ createDocument(params: {
31
+ id: DocumentID;
32
+ owner: string;
33
+ data: DocumentData | null;
34
+ unique: Uint8Array;
35
+ }): Promise<DocumentNode>;
36
+ saveDocument(params: {
37
+ id: DocumentID;
38
+ existing: DocumentNode;
39
+ data: DocumentData | null;
40
+ }): Promise<DocumentNode>;
41
+ getFieldHLCs(documentID: string): Promise<Record<string, string> | null>;
42
+ updateFieldHLCs(docID: DocumentID, fieldHLCs: Record<string, string>): Promise<void>;
43
+ };
13
44
  export type MutationContext = {
14
- db: WritableDB;
45
+ store: MutationDB;
15
46
  validators: ValidatorsRecord;
16
47
  accessChecker?: AccessChecker;
17
48
  hlc?: HLC;
18
49
  maxDriftMS?: number;
50
+ /**
51
+ * Optional post-state access gate. When provided, called once per mutation
52
+ * with the synthesized post-apply `DocumentNode` after compute but before
53
+ * any persist call. Returning `false` causes `applyMutation` to return
54
+ * `null` without writing.
55
+ */
56
+ postStateGate?: PostStateGate;
19
57
  };
20
58
  export declare function getDocumentValidator(ctx: MutationContext, id: string): Promise<DocumentValidator>;
21
59
  export declare function getTopLevelField(path: string): string;
22
- export declare function applyChangeMutation(ctx: MutationContext, mutation: ChangeDocumentMutation): Promise<DocumentNode>;
23
- export declare function applySetMutation(ctx: MutationContext, mutation: SetDocumentMutation): Promise<DocumentNode>;
24
- export declare function applyMutation(ctx: MutationContext, mutation: DocumentMutation): Promise<DocumentNode>;
60
+ export declare function applyChangeMutation(ctx: MutationContext, mutation: ChangeDocumentMutation): Promise<DocumentNode | null>;
61
+ export declare function applySetMutation(ctx: MutationContext, mutation: SetDocumentMutation): Promise<DocumentNode | null>;
62
+ export declare function applyMutation(ctx: MutationContext, mutation: DocumentMutation): Promise<DocumentNode | null>;
package/lib/apply.js CHANGED
@@ -1 +1 @@
1
- import{fromB64 as t}from"@enkaku/codec";import{applyPatches as e}from"@enkaku/patch";import{asType as a,createValidator as r}from"@enkaku/schema";import{HLC as i}from"@kubun/hlc";import{DocumentID as n}from"@kubun/id";export function getDocumentValidator(t,e){return null==t.validators[e]&&(t.validators[e]=t.db.getDocumentModel(e).then(t=>r({...t.schema,$id:e}))),t.validators[e]}function o(t,e,a){let r=i.parse(t),n=Date.now(),o=r.wallTime-n;if(o>e)throw Error(`HLC timestamp too far in the future: ${o}ms drift exceeds ${e}ms limit`);a&&a.receive(r)}export function getTopLevelField(t){let e=t.indexOf("/",1);return -1===e?t:t.slice(0,e)}export async function applyChangeMutation(t,r){let i=n.fromString(r.sub),l=i.toString(),u=await t.db.getDocument(i);if(null==u)throw Error(`Document not found: ${l}`);if(r.iss!==u.owner)throw Error("Invalid mutation issuer");if(t.accessChecker&&!await t.accessChecker(u,"write"))throw Error(`Access denied: cannot write document ${l}`);null!=t.maxDriftMS&&o(r.hlc,t.maxDriftMS,t.hlc);let c=i.model.toString(),s=await t.db.getFieldHLCs(l)??{};if(1===r.patch.length&&"replace"===r.patch[0].op&&"/"===r.patch[0].path&&null===r.patch[0].value){let e={...s,"/":r.hlc},a=Object.entries(e).every(([t,e])=>"/"===t||r.hlc>=e);return(await t.db.updateFieldHLCs(i,e),a)?await t.db.saveDocument({id:i,existing:u,data:null}):u}if(null!=s["/"]&&null===u.data)return u;let d=await getDocumentValidator(t,c),h=new Map;for(let t of r.patch){let e=getTopLevelField(t.path),a=h.get(e);null!=a?a.push(t):h.set(e,[t])}let m={...s},p=[];for(let[t,e]of h){let a=s[t];(null==a||r.hlc>a)&&(p.push(...e),m[t]=r.hlc)}if(0===p.length)return u;let f=structuredClone(u.data??{});e(f,p,!1),await t.db.updateFieldHLCs(i,m);let w=a(d,f);return await t.db.saveDocument({id:i,existing:u,data:w})}export async function applySetMutation(e,r){let i=r.aud??r.iss;if(r.iss!==i)throw Error("Invalid mutation issuer");let l=n.fromString(r.sub),u=l.toString(),c=l.model.toString();null!=e.maxDriftMS&&o(r.hlc,e.maxDriftMS,e.hlc);let[s,d]=await Promise.all([e.db.getDocument(l),getDocumentValidator(e,c)]),h=a(d,r.data),m={};for(let t of Object.keys(h))m[`/${t}`]=r.hlc;if(null===s){let a=await e.db.createDocument({id:l,owner:i,data:h,unique:t(r.unq)});return await e.db.updateFieldHLCs(l,m),a}if(s.owner!==i)throw Error(`Cannot change owner from ${s.owner} to ${i} in document: ${l.toString()}`);if(e.accessChecker&&!await e.accessChecker(s,"write"))throw Error(`Access denied: cannot write document ${l.toString()}`);let p=await e.db.getFieldHLCs(u)??{},f={...s.data??{}},w={...p},g=!1;for(let[t,e]of Object.entries(h)){let a=`/${t}`,i=p[a];(null==i||r.hlc>i)&&(f[t]=e,w[a]=r.hlc,g=!0)}if(!g)return s;await e.db.updateFieldHLCs(l,w);let b=a(d,f);return await e.db.saveDocument({id:l,existing:s,data:b})}export async function applyMutation(t,e){switch(e.typ){case"change":return await applyChangeMutation(t,e);case"set":return await applySetMutation(t,e);default:throw Error("Unsupported mutation type")}}
1
+ import{fromB64 as t}from"@enkaku/codec";import{applyPatches as e}from"@enkaku/patch";import{asType as a,createValidator as r}from"@enkaku/schema";import{HLC as o}from"@kubun/hlc";import{DocumentID as n}from"@kubun/id";export function getDocumentValidator(t,e){return null==t.validators[e]&&(t.validators[e]=t.store.getDocumentModel(e).then(t=>r({...t.schema,$id:e}))),t.validators[e]}function i(t,e,a){let r=o.parse(t),n=Date.now(),i=r.wallTime-n;if(i>e)throw Error(`HLC timestamp too far in the future: ${i}ms drift exceeds ${e}ms limit`);a&&a.receive(r)}export function getTopLevelField(t){let e=t.indexOf("/",1);return -1===e?t:t.slice(0,e)}export async function applyChangeMutation(t,r){let o=n.fromString(r.sub),l=o.toString(),u=await t.store.getDocument(o);if(null==u)throw Error(`Document not found: ${l}`);if(r.iss!==u.owner)throw Error("Invalid mutation issuer");if(t.accessChecker&&!await t.accessChecker(u,"write"))throw Error(`Access denied: cannot write document ${l}`);null!=t.maxDriftMS&&i(r.hlc,t.maxDriftMS,t.hlc);let s=o.model.toString(),c=await t.store.getFieldHLCs(l)??{};if(1===r.patch.length&&"replace"===r.patch[0].op&&"/"===r.patch[0].path&&null===r.patch[0].value){let e={...c,"/":r.hlc},a=Object.entries(e).every(([t,e])=>"/"===t||r.hlc>=e);if(a&&null!=t.postStateGate){let e={...u,data:null};if(!await t.postStateGate(e))return null}return(await t.store.updateFieldHLCs(o,e),a)?await t.store.saveDocument({id:o,existing:u,data:null}):u}if(null!=c["/"]&&null===u.data)return u;let d=await getDocumentValidator(t,s),p=new Map;for(let t of r.patch){let e=getTopLevelField(t.path),a=p.get(e);null!=a?a.push(t):p.set(e,[t])}let f={...c},m=[];for(let[t,e]of p){let a=c[t];(null==a||r.hlc>a)&&(m.push(...e),f[t]=r.hlc)}if(0===m.length)return u;let h=structuredClone(u.data??{});e(h,m,!1);let w=a(d,h);if(null!=t.postStateGate){let e={...u,data:w};if(!await t.postStateGate(e))return null}return await t.store.updateFieldHLCs(o,f),await t.store.saveDocument({id:o,existing:u,data:w})}export async function applySetMutation(e,r){let o=r.aud??r.iss;if(r.iss!==o)throw Error("Invalid mutation issuer");let l=n.fromString(r.sub),u=l.toString(),s=l.model.toString();null!=e.maxDriftMS&&i(r.hlc,e.maxDriftMS,e.hlc);let[c,d]=await Promise.all([e.store.getDocument(l),getDocumentValidator(e,s)]),p=a(d,r.data),f={};for(let t of Object.keys(p))f[`/${t}`]=r.hlc;if(null===c){if(null!=e.postStateGate){let t={id:u,model:s,owner:o,data:p,createdAt:new Date(0),updatedAt:null};if(!await e.postStateGate(t))return null}let a=await e.store.createDocument({id:l,owner:o,data:p,unique:t(r.unq)});return await e.store.updateFieldHLCs(l,f),a}if(c.owner!==o)throw Error(`Cannot change owner from ${c.owner} to ${o} in document: ${l.toString()}`);if(e.accessChecker&&!await e.accessChecker(c,"write"))throw Error(`Access denied: cannot write document ${l.toString()}`);let m=await e.store.getFieldHLCs(u)??{},h={...c.data??{}},w={...m},g=!1;for(let[t,e]of Object.entries(p)){let a=`/${t}`,o=m[a];(null==o||r.hlc>o)&&(h[t]=e,w[a]=r.hlc,g=!0)}if(!g)return c;let S=a(d,h);if(null!=e.postStateGate){let t={...c,data:S};if(!await e.postStateGate(t))return null}return await e.store.updateFieldHLCs(l,w),await e.store.saveDocument({id:l,existing:c,data:S})}export async function applyMutation(t,e){switch(e.typ){case"change":return await applyChangeMutation(t,e);case"set":return await applySetMutation(t,e);default:throw Error("Unsupported mutation type")}}
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { HLC, type HLCTimestamp } from '@kubun/hlc';
2
- export type { DocumentValidator, MutationContext, ValidatorsRecord } from './apply.js';
2
+ export type { DocumentValidator, MutationContext, MutationDB, PostStateGate, ValidatorsRecord, } from './apply.js';
3
3
  export { applyChangeMutation, applyMutation, applySetMutation, getTopLevelField } from './apply.js';
4
4
  export type { CreateChangeMutationParams, CreateRemoveMutationParams, CreateSetMutationParams, } from './create.js';
5
5
  export { createChangeMutation, createRemoveMutation, createSetMutation } from './create.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/mutation",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "see LICENSE.md",
5
5
  "keywords": [],
6
6
  "type": "module",
@@ -15,17 +15,16 @@
15
15
  ],
16
16
  "sideEffects": false,
17
17
  "dependencies": {
18
- "@enkaku/codec": "^0.14.0",
19
- "@enkaku/patch": "^0.14.0",
20
- "@enkaku/runtime": "^0.14.0",
21
- "@enkaku/schema": "^0.14.0",
22
- "@kubun/hlc": "^0.8.0",
23
- "@kubun/id": "^0.8.0"
18
+ "@enkaku/codec": "^0.15.0",
19
+ "@enkaku/patch": "^0.15.0",
20
+ "@enkaku/runtime": "^0.15.0",
21
+ "@enkaku/schema": "^0.15.0",
22
+ "@kubun/hlc": "^0.9.0",
23
+ "@kubun/id": "^0.9.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@kubun/graphql": "^0.8.0",
27
- "@kubun/db": "^0.8.0",
28
- "@kubun/protocol": "^0.8.0"
26
+ "@kubun/protocol": "^0.9.0",
27
+ "@kubun/graphql": "^0.9.0"
29
28
  },
30
29
  "scripts": {
31
30
  "build:clean": "del lib",