diff --git a/README.md b/README.md index 4ab94f7..6cab0cc 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ This is just a taster. Places you can go next: - [Input Defaults](#input-defaults) - [Input Transformation](#input-transformation) - [Input Validation](#input-validation) + - [Update](#update) - [Metadata](#metadata) - [Chaining API](#chaining-api) - [Codecs](#codecs) @@ -456,6 +457,14 @@ const circle = circle.create({ radius: -10 }) // throws ``` +### Update + +You can update records. Updating creates shallow copies of data. The validation, transformations, defaults etc. setup on the zod schema will re-run on the update function ensuring data integrity. Any errors there will be thrown. + +```ts +const circleUpdated = circle.update(circle, { radius: 5 }) +``` + ### Metadata The controller gives you access to metadata about your record: diff --git a/src/record/runtime.ts b/src/record/runtime.ts index 09daaa4..5d1224a 100644 --- a/src/record/runtime.ts +++ b/src/record/runtime.ts @@ -100,8 +100,14 @@ export function record( return current.schema.passthrough().parse(data) as object }, //eslint-disable-next-line - is$: (value: unknown) => is(value, symbol), - is: (value: unknown) => is(value, symbol), + is$: (value) => is(value, symbol), + is: (record) => is(record, symbol), + update: (record, changes) => { + return controller.create({ + ...record, + ...changes, + }) as object + }, from: { json: (json: string) => { const data = tryOrNull(() => JSON.parse(json) as object) diff --git a/src/record/types/controller.ts b/src/record/types/controller.ts index 2e579e8..7487227 100644 --- a/src/record/types/controller.ts +++ b/src/record/types/controller.ts @@ -1,5 +1,5 @@ import { SomeSchema, SomeSchemaDef } from '../../core/internal.js' -import { Encoder, SomeName, StoredRecords } from '../../core/types.js' +import { Encoder, OmitTag, SomeName, StoredRecords } from '../../core/types.js' import { OmitRequired, Rest } from '../../lib/utils.js' import { z } from '../../lib/z/index.js' import { SomeDecodeOrThrowJson, SomeDecoderJson, SomeDefaultsProvider, SomeEncoderJson } from './internal.js' @@ -24,9 +24,13 @@ export type SomeRecordController = { } name: string schema: SomeSchema + // !! HACK not-using any here results in test type errors that I don't understand yet. // eslint-disable-next-line - is: (value: any) => boolean + is: (record: any) => boolean is$: (value: unknown) => boolean + // !! HACK not-using any here results in test type errors that I don't understand yet. + // eslint-disable-next-line + update: (record: any, changes: object) => object // eslint-disable-next-line create: (params?: any) => any from: { @@ -92,6 +96,11 @@ export type RecordController, changes: Partial>>): StoredRecord.GetType /** * Decoders for this record. Decoders are used to transform other representations of your record back into a record instance. */ @@ -101,13 +110,13 @@ export type RecordController null | StoredRecord.GetType + json(value: string): null | StoredRecord.GetType /** * Decode JSON into this record. Throws if it fails for any reason. * * @remarks This is a built in decoder. */ - jsonOrThrow: (value: string) => StoredRecord.GetType + jsonOrThrow(value: string): StoredRecord.GetType } & Decoders // & { // [I in IndexKeys as AsString]: Decoder,V> @@ -121,7 +130,7 @@ export type RecordController) => string + json(record: StoredRecord.GetType): string } & Encoders /** * Strict predicate/type guard for this record. diff --git a/tests/record/other/update.spec.ts b/tests/record/other/update.spec.ts new file mode 100644 index 0000000..169fb39 --- /dev/null +++ b/tests/record/other/update.spec.ts @@ -0,0 +1,32 @@ +import { Alge } from '../../../src/index.js' +import { A, a } from '../../__helpers__.js' +import { expectType } from 'tsd' +import { z } from 'zod' + +it(`updates record by copy`, () => { + // eslint-disable-next-line + expectType<(data: A, changes: Partial) => A>(A.update) + const a_ = A.update(a, { m: `updated` }) + expect(a).toMatchObject({ m: `m` }) + expect(a_).not.toBe(a) + expect(a_).toEqual({ _tag: `A`, m: `updated`, _: { tag: `A`, symbol: A._.symbol } }) +}) + +it(`updates triggers validate etc.`, () => { + const A = Alge.record(`A`, { + m: z.string().regex(/abc/), + }) + const a = A.create({ m: `abc` }) + expect(() => A.update(a, { m: `updated` })).toThrowErrorMatchingInlineSnapshot(` + "[ + { + "validation": "regex", + "code": "invalid_string", + "message": "Invalid", + "path": [ + "m" + ] + } + ]" + `) +})