@kubun/mutation 0.9.0 → 0.10.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/apply.d.ts +6 -7
- package/lib/apply.js +1 -1
- package/lib/create.d.ts +3 -0
- package/lib/create.js +1 -1
- package/lib/errors.d.ts +11 -0
- package/lib/errors.js +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -1
- package/lib/operations.d.ts +1 -0
- package/lib/operations.js +1 -1
- package/package.json +9 -9
package/lib/apply.d.ts
CHANGED
|
@@ -5,12 +5,6 @@ import { DocumentID } from '@kubun/id';
|
|
|
5
5
|
import type { ChangeDocumentMutation, DocumentData, DocumentModel, DocumentMutation, DocumentNode, SetDocumentMutation } from '@kubun/protocol';
|
|
6
6
|
export type DocumentValidator = Validator<DocumentData>;
|
|
7
7
|
export type ValidatorsRecord = Record<string, Promise<DocumentValidator>>;
|
|
8
|
-
export type AccessChecker = (doc: {
|
|
9
|
-
id: string;
|
|
10
|
-
model: string;
|
|
11
|
-
owner: string;
|
|
12
|
-
data: DocumentData | null;
|
|
13
|
-
}, permissionType: 'read' | 'write') => Promise<boolean>;
|
|
14
8
|
/**
|
|
15
9
|
* Post-state access gate evaluated AFTER the post-apply `DocumentNode` is
|
|
16
10
|
* computed in memory but BEFORE any persist call. Returning `false` instructs
|
|
@@ -44,7 +38,12 @@ export type MutationDB = {
|
|
|
44
38
|
export type MutationContext = {
|
|
45
39
|
store: MutationDB;
|
|
46
40
|
validators: ValidatorsRecord;
|
|
47
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Capability gate for non-owner writes. When the mutation issuer is not the
|
|
43
|
+
* document owner, the write is only applied if this returns `true`. Absent an
|
|
44
|
+
* injected checker, only the owner may write (owner-only safe default).
|
|
45
|
+
*/
|
|
46
|
+
checkWriteAccess?: (doc: DocumentNode, mutation: DocumentMutation) => Promise<boolean>;
|
|
48
47
|
hlc?: HLC;
|
|
49
48
|
maxDriftMS?: number;
|
|
50
49
|
/**
|
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
|
|
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 l}from"@kubun/hlc";import{DocumentID as o}from"@kubun/id";import{WriteAccessDeniedError as n}from"./errors.js";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){let a=l.parse(t),r=Date.now(),o=a.wallTime-r;if(o>e)throw Error(`HLC timestamp too far in the future: ${o}ms drift exceeds ${e}ms limit`)}export function getTopLevelField(t){let e=t.indexOf("/",1);return -1===e?t:t.slice(0,e)}export async function applyChangeMutation(t,r){let l=o.fromString(r.sub),u=l.toString(),s=await t.store.getDocument(l);if(null==s)throw Error(`Document not found: ${u}`);if(r.iss!==s.owner&&!(null!=t.checkWriteAccess&&await t.checkWriteAccess(s,r)))throw new n(u);null!=t.maxDriftMS&&i(r.hlc,t.maxDriftMS);let c=l.model.toString(),p=await t.store.getFieldHLCs(u)??{};if(1===r.patch.length&&"replace"===r.patch[0].op&&"/"===r.patch[0].path&&null===r.patch[0].value){let e={...p,"/":r.hlc},a=Object.entries(e).every(([t,e])=>"/"===t||r.hlc>=e);if(a&&null!=t.postStateGate){let e={...s,data:null};if(!await t.postStateGate(e))return null}return(await t.store.updateFieldHLCs(l,e),a)?await t.store.saveDocument({id:l,existing:s,data:null}):s}if(null!=p["/"]&&null===s.data)return s;let f=await getDocumentValidator(t,c),d=new Map;for(let t of r.patch){let e=getTopLevelField(t.path),a=d.get(e);null!=a?a.push(t):d.set(e,[t])}let m={...p},h=[];for(let[t,e]of d){let a=p[t];(null==a||r.hlc>a)&&(h.push(...e),m[t]=r.hlc)}if(0===h.length)return s;let w=structuredClone(s.data??{});e(w,h,!1);let g=a(f,w);if(null!=t.postStateGate){let e={...s,data:g};if(!await t.postStateGate(e))return null}return await t.store.updateFieldHLCs(l,m),await t.store.saveDocument({id:l,existing:s,data:g})}export async function applySetMutation(e,r){let l=r.aud??r.iss,u=o.fromString(r.sub),s=u.toString(),c=u.model.toString();null!=e.maxDriftMS&&i(r.hlc,e.maxDriftMS);let[p,f]=await Promise.all([e.store.getDocument(u),getDocumentValidator(e,c)]),d=a(f,r.data),m={};for(let t of Object.keys(d))m[`/${t}`]=r.hlc;let h={id:s,model:c,owner:l,data:d,createdAt:new Date(0),updatedAt:null};if(r.iss!==l){let t=p??h;if(!(null!=e.checkWriteAccess&&await e.checkWriteAccess(t,r)))throw new n(s)}if(null===p){if(null!=e.postStateGate&&!await e.postStateGate(h))return null;let a=await e.store.createDocument({id:u,owner:l,data:d,unique:t(r.unq)});return await e.store.updateFieldHLCs(u,m),a}if(p.owner!==l)throw Error(`Cannot change owner from ${p.owner} to ${l} in document: ${u.toString()}`);let w=await e.store.getFieldHLCs(s)??{},g={...p.data??{}},S={...w},D=!1;for(let[t,e]of Object.entries(d)){let a=`/${t}`,l=w[a];(null==l||r.hlc>l)&&(g[t]=e,S[a]=r.hlc,D=!0)}if(!D)return p;let x=a(f,g);if(null!=e.postStateGate){let t={...p,data:x};if(!await e.postStateGate(t))return null}return await e.store.updateFieldHLCs(u,S),await e.store.saveDocument({id:u,existing:p,data:x})}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/create.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type CreateSetMutationParams<Data extends DocumentData = DocumentData> =
|
|
|
9
9
|
modelID: DocumentModelID | string;
|
|
10
10
|
owner?: string;
|
|
11
11
|
unique: Uint8Array;
|
|
12
|
+
cap?: string | Array<string>;
|
|
12
13
|
};
|
|
13
14
|
export declare function createSetMutation<Data extends DocumentData = DocumentData>(params: CreateSetMutationParams<Data>): Promise<SetDocumentMutation>;
|
|
14
15
|
export type CreateChangeMutationParams = {
|
|
@@ -16,11 +17,13 @@ export type CreateChangeMutationParams = {
|
|
|
16
17
|
hlc: HLC;
|
|
17
18
|
issuer: string;
|
|
18
19
|
patch: Array<PatchOperation>;
|
|
20
|
+
cap?: string | Array<string>;
|
|
19
21
|
};
|
|
20
22
|
export declare function createChangeMutation(params: CreateChangeMutationParams): ChangeDocumentMutation;
|
|
21
23
|
export type CreateRemoveMutationParams = {
|
|
22
24
|
docID: string;
|
|
23
25
|
hlc: HLC;
|
|
24
26
|
issuer: string;
|
|
27
|
+
cap?: string | Array<string>;
|
|
25
28
|
};
|
|
26
29
|
export declare function createRemoveMutation(params: CreateRemoveMutationParams): ChangeDocumentMutation;
|
package/lib/create.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toB64 as e}from"@enkaku/codec";import{HLC as t}from"@kubun/hlc";import{DocumentID as
|
|
1
|
+
import{toB64 as e}from"@enkaku/codec";import{HLC as t}from"@kubun/hlc";import{DocumentID as a}from"@kubun/id";export async function createSetMutation(c){let n=c.issuer,u=c.owner??n,r=await a.create(c.modelID,u,c.unique);return{typ:"set",iss:n,aud:u,sub:r.toString(),data:c.data,hlc:t.serialize(c.hlc.now()),unq:e(c.unique),...null!=c.cap?{cap:c.cap}:{}}}export function createChangeMutation(e){return{typ:"change",iss:e.issuer,sub:a.from(e.docID).toString(),patch:e.patch,hlc:t.serialize(e.hlc.now()),...null!=e.cap?{cap:e.cap}:{}}}export function createRemoveMutation(e){return{typ:"change",iss:e.issuer,sub:e.docID,patch:[{op:"replace",path:"/",value:null}],hlc:t.serialize(e.hlc.now()),...null!=e.cap?{cap:e.cap}:{}}}
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by the mutation apply path when a non-owner writer's capability
|
|
3
|
+
* check fails. Tagged with the protocol-level error code `KB07` so callers
|
|
4
|
+
* — particularly the RPC handler — can translate the throw into a
|
|
5
|
+
* structured wire response without losing the reason.
|
|
6
|
+
*/
|
|
7
|
+
export declare class WriteAccessDeniedError extends Error {
|
|
8
|
+
readonly code: "KB07";
|
|
9
|
+
readonly docID: string;
|
|
10
|
+
constructor(docID: string);
|
|
11
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class WriteAccessDeniedError extends Error{code="KB07";docID;constructor(e){super(`Access denied: cannot write document ${e}`),this.name="WriteAccessDeniedError",this.docID=e}}
|
package/lib/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export type { DocumentValidator, MutationContext, MutationDB, PostStateGate, Val
|
|
|
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';
|
|
6
|
+
export { WriteAccessDeniedError } from './errors.js';
|
|
6
7
|
export type { CreateDocumentParams, CreateMutationOperationsParams, MutationOperations, RemoveDocumentParams, SetDocumentParams, UpdateDocumentParams, } from './operations.js';
|
|
7
8
|
export { convertPatchInput, createMutationOperations } from './operations.js';
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{HLC}from"@kubun/hlc";export{applyChangeMutation,applyMutation,applySetMutation,getTopLevelField}from"./apply.js";export{createChangeMutation,createRemoveMutation,createSetMutation}from"./create.js";export{convertPatchInput,createMutationOperations}from"./operations.js";
|
|
1
|
+
export{HLC}from"@kubun/hlc";export{applyChangeMutation,applyMutation,applySetMutation,getTopLevelField}from"./apply.js";export{createChangeMutation,createRemoveMutation,createSetMutation}from"./create.js";export{WriteAccessDeniedError}from"./errors.js";export{convertPatchInput,createMutationOperations}from"./operations.js";
|
package/lib/operations.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type CreateMutationOperationsParams<T> = {
|
|
|
6
6
|
issuer: string;
|
|
7
7
|
hlc: HLC;
|
|
8
8
|
getRandomValues: GetRandomValues;
|
|
9
|
+
cap?: string | Array<string>;
|
|
9
10
|
processSetMutation(mutation: SetDocumentMutation, transactionID?: string | null): Promise<T>;
|
|
10
11
|
processChangeMutation(mutation: ChangeDocumentMutation, transactionID?: string | null): Promise<T>;
|
|
11
12
|
};
|
package/lib/operations.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createChangeMutation as t,createRemoveMutation as e,createSetMutation as r}from"./create.js";export function createMutationOperations(n){let{issuer:a,hlc:c,getRandomValues:u,
|
|
1
|
+
import{createChangeMutation as t,createRemoveMutation as e,createSetMutation as r}from"./create.js";export function createMutationOperations(n){let{issuer:a,hlc:c,getRandomValues:u,cap:o,processSetMutation:i,processChangeMutation:m}=n;return{async createDocument({modelID:t,data:e,transactionID:n}){let m=u(new Uint8Array(12)),s=await r({issuer:a,hlc:c,modelID:t,data:e,unique:m,cap:o});return await i(s,n)},async setDocument({modelID:t,unique:e,data:n,transactionID:u}){let m=await r({issuer:a,hlc:c,modelID:t,data:n,unique:e,cap:o});return await i(m,u)},async updateDocument({docID:e,patch:r,transactionID:n}){let u=t({issuer:a,hlc:c,docID:e,patch:r,cap:o});return await m(u,n)},async removeDocument({docID:t,transactionID:r}){let n=e({docID:t,issuer:a,hlc:c,cap:o});return await m(n,r)}}}export function convertPatchInput(t){return t.map(t=>{let[e,r]=Object.entries(t)[0];return{...r,op:e}})}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/mutation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"type": "module",
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
],
|
|
16
16
|
"sideEffects": false,
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@enkaku/codec": "^0.
|
|
19
|
-
"@enkaku/patch": "^0.
|
|
20
|
-
"@enkaku/runtime": "^0.
|
|
21
|
-
"@enkaku/schema": "^0.
|
|
22
|
-
"@kubun/hlc": "^0.
|
|
23
|
-
"@kubun/id": "^0.
|
|
18
|
+
"@enkaku/codec": "^0.16.0",
|
|
19
|
+
"@enkaku/patch": "^0.16.0",
|
|
20
|
+
"@enkaku/runtime": "^0.16.0",
|
|
21
|
+
"@enkaku/schema": "^0.16.0",
|
|
22
|
+
"@kubun/hlc": "^0.10.1",
|
|
23
|
+
"@kubun/id": "^0.10.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@kubun/protocol": "^0.
|
|
27
|
-
"@kubun/graphql": "^0.
|
|
26
|
+
"@kubun/protocol": "^0.10.0",
|
|
27
|
+
"@kubun/graphql": "^0.10.1"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build:clean": "del lib",
|