import type { IContainEntitiesAndLinks, LinkParams, CreateParams, UpdateParams, UpdateOpts, RuleParams, UniqueKeys, ResolveEntityAttrs, } from './schemaTypes.ts'; type Action = | 'create' | 'update' | 'link' | 'unlink' | 'delete' | 'merge' | 'ruleParams'; type EType = string; type Id = string; type Args = any; type LookupRef = [string, any]; type Lookup = string; type Opts = UpdateOpts; export type Op = [Action, EType, Id | LookupRef, Args, Opts?]; export interface TransactionChunk< Schema extends IContainEntitiesAndLinks, EntityName extends keyof Schema['entities'], > { __ops: Op[]; __etype: EntityName; /** * Create objects. Throws an error if the object with the provided ID already * exists. */ create: ( args: CreateParams, ) => TransactionChunk; /** * Create and update objects. By default works in upsert mode (will create * entity if that doesn't exist). Can be optionally put into "strict update" * mode by providing { upsert: false } option as second argument: * * @example * const goalId = id(); * // upsert * db.tx.goals[goalId].update({title: "Get fit", difficulty: 5}) * * // strict update * db.tx.goals[goalId].update({title: "Get fit"}, {upsert: false}) */ update: ( args: UpdateParams, opts?: UpdateOpts, ) => TransactionChunk; /** * Link two objects together * * @example * const goalId = id(); * const todoId = id(); * db.transact([ * db.tx.goals[goalId].update({title: "Get fit"}), * db.tx.todos[todoId].update({title: "Go on a run"}), * db.tx.goals[goalId].link({todos: todoId}), * ]) * * // Now, if you query: * useQuery({ goals: { todos: {} } }) * // You'll get back: * * // { goals: [{ title: "Get fit", todos: [{ title: "Go on a run" }]} */ link: ( args: LinkParams, ) => TransactionChunk; /** * Unlink two objects * @example * // to "unlink" a todo from a goal: * db.tx.goals[goalId].unlink({todos: todoId}) */ unlink: ( args: LinkParams, ) => TransactionChunk; /** * Delete an object, alongside all of its links. * * @example * db.tx.goals[goalId].delete() */ delete: () => TransactionChunk; /** * * Similar to `update`, but instead of overwriting the current value, it will merge the provided values into the current value. * * This is useful for deeply nested, document-style values, or for updating a single attribute at an arbitrary depth without overwriting the rest of the object. * * For example, if you have a goal with a nested `metrics` object: * * ```js * goal = { name: "Get fit", metrics: { progress: 0.3 } } * ``` * * You can update the `progress` attribute like so: * * ```js * db.tx.goals[goalId].merge({ metrics: { progress: 0.5 }, category: "Fitness" }) * ``` * * And the resulting object will be: * * ```js * goal = { name: "Get fit", metrics: { progress: 0.5 }, category: "Fitness" } * ``` * * @example * const goalId = id(); * db.tx.goals[goalId].merge({title: "Get fitter"}) */ merge: ( args: { [attribute: string]: any; }, opts?: UpdateOpts, ) => TransactionChunk; ruleParams: (args: RuleParams) => TransactionChunk; } // This is a hack to get typescript to enforce that // `allTransactionChunkKeys` contains all the keys of `TransactionChunk` type TransactionChunkKey = keyof TransactionChunk; function getAllTransactionChunkKeys(): Set { const v: any = 1; const _dummy: TransactionChunk = { __etype: v, __ops: v, create: v, update: v, link: v, unlink: v, delete: v, merge: v, ruleParams: v, }; return new Set(Object.keys(_dummy)) as Set; } const allTransactionChunkKeys = getAllTransactionChunkKeys(); export type ETypeChunk< Schema extends IContainEntitiesAndLinks, EntityName extends keyof Schema['entities'], > = { [id: Id]: TransactionChunk; } & { lookup: >( attrName: Name, value: ResolveEntityAttrs[Name], ) => TransactionChunk; }; export type TxChunk> = { [EntityName in keyof Schema['entities']]: ETypeChunk; }; function transactionChunk( etype: EType, id: Id | LookupRef, prevOps: Op[], ): TransactionChunk { const target = { __etype: etype, __ops: prevOps, }; return new Proxy(target as unknown as TransactionChunk, { get: (_target, cmd: keyof TransactionChunk) => { if (cmd === '__ops') return prevOps; if (cmd === '__etype') return etype; if (!allTransactionChunkKeys.has(cmd)) { return undefined; } return (args: Args, opts?: Opts) => { return transactionChunk(etype, id, [ ...prevOps, opts ? [cmd, etype, id, args, opts] : [cmd, etype, id, args], ]); }; }, }); } /** * Creates a lookup to use in place of an id in a transaction * * @example * db.tx.users[lookup('email', 'lyndon@example.com')].update({name: 'Lyndon'}) */ export function lookup(attribute: string, value: any): Lookup { return `lookup__${attribute}__${JSON.stringify(value)}`; } export function isLookup(k: string): boolean { return k.startsWith('lookup__'); } export function parseLookup(k: string): LookupRef { const [_, attribute, ...vJSON] = k.split('__'); return [attribute, JSON.parse(vJSON.join('__'))]; } function etypeChunk(etype: EType): ETypeChunk { return new Proxy( { __etype: etype, } as unknown as ETypeChunk, { get(_target, cmd: Id) { if (cmd === 'lookup') { return (attrName: string, value: any) => transactionChunk(etype, parseLookup(lookup(attrName, value)), []); } if (cmd === '__etype') return etype; const id = cmd; if (isLookup(id)) { return transactionChunk(etype, parseLookup(id), []); } return transactionChunk(etype, id, []); }, }, ); } export function txInit< Schema extends IContainEntitiesAndLinks, >(): TxChunk { return new Proxy( {}, { get(_target, ns: EType) { return etypeChunk(ns); }, }, ) as any; } /** * A handy builder for changes. * * You must start with the `namespace` you want to change: * * @example * db.tx.goals[goalId].update({title: "Get fit"}) * // Note: you don't need to create `goals` ahead of time. */ export const tx = txInit(); export function getOps(x: TransactionChunk): Op[] { return x.__ops; }