UNPKG

temporeest

Version:
209 lines (183 loc) 5.92 kB
import { INode, NodeSpecWithCreate, Changeset, CreateChangeset, UpdateChangeset, DeleteChangeset, ChangesetOptions, IModel, ModelSpecWithCreate, Context, OptimisticPromise, } from '@aphro/context-runtime-ts'; import { nanoid } from 'nanoid'; import { commit } from './commit.js'; export interface IMutationBuilder<M extends IModel<D>, D extends Object> { readonly ctx: Context; toChangesets(options?: ChangesetOptions): [Changeset<M, D>, ...Changeset<any, any>[]] & { save: () => OptimisticPromise<[M, ...any[]]>; save0: () => [M, ...any[]]; }; // TODO: remove this once we get mutations generation complete // we don't need `set` for `delete` set(newData: Partial<D>): this; addExtraChangesets(changesets?: Changeset<any, any>[]): this; } export interface ICreateOrUpdateBuilder<M extends IModel<D>, D extends Object> extends IMutationBuilder<M, D> { set(newData: Partial<D>): this; } // TODO: and if we want to enable transactions... abstract class MutationBuilder<M extends IModel<D>, D extends Object> implements IMutationBuilder<M, D> { #extraChangesets: Changeset<any, any>[] = []; constructor( public readonly ctx: Context, protected spec: ModelSpecWithCreate<M, D>, protected data: Partial<D>, ) {} toChangesets(): [Changeset<M, D>, ...Changeset<any, any>[]] & { save: () => OptimisticPromise<[M, ...any[]]>; save0: () => [M, ...any[]]; } { const cs = this.toChangesetImpl(); const ret = [cs as Changeset<M, D>, ...this.#extraChangesets]; // @ts-ignore ret.save = () => { return commit(this.ctx, ret); }; // @ts-ignore ret.save0 = () => { return commit(this.ctx, ret).optimistic; }; // @ts-ignore return ret; } protected abstract toChangesetImpl(): Omit<Changeset<M, D>, 'save' | 'save0'>; set(newData: Partial<D>): this { throw new Error('You cannot call `set` when deleting something'); } addExtraChangesets(changesets?: Changeset<any, any>[]): this { if (changesets == null) { return this; } changesets.forEach(cs => this.#extraChangesets.push(cs)); return this; } /** * Models are updated immediately in-memory. A reference to the model * is returned for the case of creates. * * @returns reference to the model */ save(): OptimisticPromise<M[]> { return this.toChangesets().save(); } } abstract class CreateOrUpdateBuilder<M extends IModel<D>, D extends Object> extends MutationBuilder< M, D > { set(newData: Partial<D>): this { (Object.entries(newData) as [keyof Partial<D>, any][]).forEach(e => { if (e[1] === undefined) { delete newData[e[0]]; } }); this.data = { ...this.data, ...newData, }; return this; } } export class CreateMutationBuilder< M extends IModel<D>, D extends Object, > extends CreateOrUpdateBuilder<M, D> { constructor(ctx: Context, spec: ModelSpecWithCreate<M, D>) { super(ctx, spec, {}); } toChangesetImpl(): Omit<CreateChangeset<M, D>, 'save' | 'save0'> { let primaryKey: any = this.spec.type === 'node' ? this.data[this.spec.primaryKey as keyof D] : // @ts-ignore this.data.id1 + '-' + this.data.id2; if (primaryKey == null && this.spec.type === 'node') { // set it automagically? I think that is reasonable. primaryKey = this.data[this.spec.primaryKey as keyof D] = nanoid() as any; } return { type: 'create', updates: this.data, spec: this.spec, // TODO: this is _not_ guaranteed to be an SID... // We either need a validation step to force primary keys to be SIDs // or we need to allow non-sids as primary keys id: primaryKey, }; } } export class UpdateMutationBuilder< M extends IModel<D>, D extends Object, > extends CreateOrUpdateBuilder<M, D> { constructor(ctx: Context, spec: ModelSpecWithCreate<M, D>, private model: M) { super(ctx, spec, {}); } toChangesetImpl(): Omit<UpdateChangeset<M, D>, 'save' | 'save0'> { return { type: 'update', updates: this.data, spec: this.spec, model: this.model, id: this.model.id, }; } } export class DeleteMutationBuilder<M extends IModel<D>, D extends Object> extends MutationBuilder< M, D > { constructor(ctx: Context, spec: ModelSpecWithCreate<M, D>, private model: M) { super(ctx, spec, {}); } toChangesetImpl(): Omit<DeleteChangeset<M, D>, 'save' | 'save0'> { return { type: 'delete', model: this.model, spec: this.spec, id: this.model.id, }; } } // TODO: we'd likely want to split backends out into plugins so we don't have to pull in knex and other deps. // for a future time. // --- // and now it is easier to see Facebook's CTO's argument against introducing the concepts of packages and namespaces. // It is a lot of extra mental gymnastics on top of writing code -- but those gymnastics make your software // flexible for the inevitable changes that come in the future. Of course if your philosophy is always YAGNI // then YAGNI (modules) until you need it. FB made it 18 years not needing them but now they need them across // the entire 100s of millions of SLOC codebase. So what do? // export class SQLMutator<T extends Object, M extends IModel<T>> extends MutatorBase<T, M> { // protected compileQuery(): { text: string; bindings: any[] } { // // Invoke Knex to create an insert or create statement. // } // } /* async save(): Promise<M> { const query = this.compileQuery(); await __internalConfig.resolver .type(this.spec.storage.type) .engine(this.spec.storage.engine) .tablish(this.spec.storage.tablish) .exec(query.text, query.bindings); // TODO: cache layer... // update existing models... // what about persist log? this.spec.createFrom(this.data); } */