diff --git a/.campaign/tgz/SHA256SUMS b/.campaign/tgz/SHA256SUMS index f78f4a2..fd6c1f9 100644 --- a/.campaign/tgz/SHA256SUMS +++ b/.campaign/tgz/SHA256SUMS @@ -1,6 +1,6 @@ abdbff87d78af3f605b465b1104c9a57fa859fbc14d8bb9ba396a49869269d99 antelopejs-api-6afe3e2.tgz 0a51c9c2da58c2586bfa5095b1da92a7b74130acf1e64820bfd5f98eaa4200f9 antelopejs-core-184eb90.tgz -8e017528574748687b246df07affa6cbcfbe931cc6086fcb5dd2c7a28c1f0d8e antelopejs-interface-api-015a93a.tgz +1a29379d519467fdd438ee411a93519956f1e024d4f457f0d447cac67af61099 antelopejs-interface-api-fcc62d2.tgz fa0a3cfa243d0c78cd29d45839ae61bbbce5615a7c8d8247755b0cd91c8bedc6 antelopejs-interface-api-util-639f38b.tgz 30391718e65481815db5b5bb10e7eabb1feac13165005ee46e54b592f3676c69 antelopejs-interface-core-f7df541.tgz ccdc833729f50825d3567047a22bf9c68f47a38ca3527117756fbd80f380fabd antelopejs-interface-database-724ff72.tgz diff --git a/.campaign/tgz/antelopejs-interface-api-015a93a.tgz b/.campaign/tgz/antelopejs-interface-api-015a93a.tgz deleted file mode 100644 index a14002a..0000000 Binary files a/.campaign/tgz/antelopejs-interface-api-015a93a.tgz and /dev/null differ diff --git a/.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz b/.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz new file mode 100644 index 0000000..19969c5 Binary files /dev/null and b/.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz differ diff --git a/README.md b/README.md index c4b3df0..b22e968 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ The package declares these `@antelopejs/*` peer dependencies: | Package | Supported | Tested with | | ------------------------------------------- | ----------------- | ------------------------------------------------------------------------- | -| `@antelopejs/interface-api` | `>=0.3.0 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz` | +| `@antelopejs/interface-api` | `>=0.3.0 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz` | | `@antelopejs/interface-api-util` | `>=0.1.3 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz` | | `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz` | | `@antelopejs/interface-database` | `>=0.1.10 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz` | diff --git a/package.json b/package.json index 3713ba2..e6f15db 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "devDependencies": { "@antelopejs/api": "file:.campaign/tgz/antelopejs-api-6afe3e2.tgz", "@antelopejs/core": "file:.campaign/tgz/antelopejs-core-184eb90.tgz", - "@antelopejs/interface-api": "file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz", + "@antelopejs/interface-api": "file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz", "@antelopejs/interface-api-util": "file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz", "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz", "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz", @@ -109,7 +109,7 @@ "overrides": { "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz", "@antelopejs/interface-database-decorators": "file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz", - "@antelopejs/interface-api": "file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz", + "@antelopejs/interface-api": "file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz", "@antelopejs/interface-api-util": "file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz", "@antelopejs/api": "file:.campaign/tgz/antelopejs-api-6afe3e2.tgz", "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2d9042..259de5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: overrides: '@antelopejs/interface-database': file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz '@antelopejs/interface-database-decorators': file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz - '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz + '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz '@antelopejs/interface-api-util': file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz '@antelopejs/api': file:.campaign/tgz/antelopejs-api-6afe3e2.tgz '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz @@ -18,11 +18,11 @@ importers: .: dependencies: '@antelopejs/interface-api': - specifier: file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz - version: file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) + specifier: file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz + version: file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) '@antelopejs/interface-api-util': specifier: file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz - version: file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) + version: file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) '@antelopejs/interface-core': specifier: file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz version: file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz @@ -31,7 +31,7 @@ importers: version: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) '@antelopejs/interface-database-decorators': specifier: file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz - version: file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)(@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)) + version: file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)(@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)) devDependencies: '@antelopejs/api': specifier: file:.campaign/tgz/antelopejs-api-6afe3e2.tgz @@ -118,8 +118,8 @@ packages: '@antelopejs/interface-api': '>=0.3.0 <1.0.0' '@antelopejs/interface-core': '>=0.2.4 <1.0.0' - '@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz': - resolution: {integrity: sha512-G7BRef+991Dn+O4zmxGE8f8KN5Xfj5GQa+W3LQo0f+XHhRUCz2E+brTIonhjV7HJ5Z+lFNrm4StPytwNIUxA5Q==, tarball: file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz} + '@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz': + resolution: {integrity: sha512-6A2dS+0P6O+AGI9Jjq+6vGflWsDC3S6BinYeNiqvQxK2ggebzt1sq1jstRmlFZWfW5/lxemjBmtwCtfXzLfXVA==, tarball: file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz} version: 0.3.0 peerDependencies: '@antelopejs/interface-core': '>=0.4.0 <1.0.0' @@ -2836,8 +2836,8 @@ snapshots: '@antelopejs/api@file:.campaign/tgz/antelopejs-api-6afe3e2.tgz(@antelopejs/core@file:.campaign/tgz/antelopejs-core-184eb90.tgz)': dependencies: - '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) - '@antelopejs/interface-api-util': file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) + '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) + '@antelopejs/interface-api-util': file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz reflect-metadata: 0.2.2 ws: 8.22.1 @@ -2862,12 +2862,12 @@ snapshots: semver: 7.7.4 typescript: 5.9.3 - '@antelopejs/interface-api-util@file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)': + '@antelopejs/interface-api-util@file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)': dependencies: - '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) + '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz - '@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)': + '@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)': dependencies: '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz @@ -2875,9 +2875,9 @@ snapshots: dependencies: reflect-metadata: 0.2.2 - '@antelopejs/interface-database-decorators@file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)(@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))': + '@antelopejs/interface-database-decorators@file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz(@antelopejs/interface-api@file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)(@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz))': dependencies: - '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) + '@antelopejs/interface-api': file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz '@antelopejs/interface-database': file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) randomstring: 1.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0e3a33c..91985f3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,7 @@ overrides: "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz" "@antelopejs/interface-database-decorators": "file:.campaign/tgz/antelopejs-interface-database-decorators-1234214.tgz" - "@antelopejs/interface-api": "file:.campaign/tgz/antelopejs-interface-api-015a93a.tgz" + "@antelopejs/interface-api": "file:.campaign/tgz/antelopejs-interface-api-fcc62d2.tgz" "@antelopejs/interface-api-util": "file:.campaign/tgz/antelopejs-interface-api-util-639f38b.tgz" "@antelopejs/api": "file:.campaign/tgz/antelopejs-api-6afe3e2.tgz" "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz" diff --git a/src/components.ts b/src/components.ts index 9d9c9db..96c65d8 100644 --- a/src/components.ts +++ b/src/components.ts @@ -37,6 +37,7 @@ import { isString } from "./internal/guards"; import * as joined from "./internal/joined"; import * as list from "./internal/list"; import * as properties from "./internal/properties"; +import { fieldsOf } from "./internal/prototypes"; import { ReadProperties as readProperties } from "./internal/read-properties"; import * as tables from "./internal/tables"; import * as validation from "./internal/validation"; @@ -271,7 +272,7 @@ export namespace Query { meta: DataAPIMeta, ): ModelInstance { assert(meta.modelKey, 500, "Missing model key."); - const model: unknown = obj[meta.modelKey]; + const model = fieldsOf(obj)[meta.modelKey]; assert( tables.isModelInstance(model), 500, diff --git a/src/index.ts b/src/index.ts index 9b1c149..f38741d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,7 +84,7 @@ export function DataController< ? { endpoint: key, callback: val } : { endpoint: key, ...val }; meta.addEndpoint(key, entry); - c.prototype[key] = entry.callback.func; + Object.assign(c.prototype, { [key]: entry.callback.func }); } // SAFETY: for every own key of def, the loop above installs the function ExtractCallback names (def[key].func, or def[key].callback.func for an entry with options) on c.prototype, which covers the members of ExtractDefCallbacks

; c extends base and declares table itself. return c as Class & TableHolder> & Base; diff --git a/src/internal/properties.ts b/src/internal/properties.ts index 2332ac4..c4a526e 100644 --- a/src/internal/properties.ts +++ b/src/internal/properties.ts @@ -7,7 +7,7 @@ import type { RowFields, WritableAccessFields, } from "../metadata"; -import { prototypeOf } from "./prototypes"; +import { fieldsOf, prototypeOf } from "./prototypes"; function writableInBody( writable: WritableAccessFields, @@ -20,7 +20,7 @@ function writableInBody( } function writeTargetDefaults( - instance: Parameters[0], + instance: RowFields, meta: DataAPIMeta, dbData: RowFields, ) { @@ -47,7 +47,7 @@ export function WriteProperties( const writable = existingDBData ? writableInBody(actionWritable, bodyData) : actionWritable; - const instance = { ...obj }; + const instance = fieldsOf({ ...obj }); const dbData: RowFields = existingDBData || {}; Object.setPrototypeOf(dbData, prototypeOf(meta.tableClass)); if (!existingDBData) { diff --git a/src/internal/prototypes.ts b/src/internal/prototypes.ts index 702b77c..9ab5494 100644 --- a/src/internal/prototypes.ts +++ b/src/internal/prototypes.ts @@ -3,8 +3,25 @@ import type { Class } from "@antelopejs/interface-core/decorators"; import type { DataControllerInstance } from "./controller-types"; +import type { RowFields } from "./field-types"; import { isClass, isObject } from "./guards"; +/** + * A controller instance read by field name: an object answers every string key, + * with `undefined` for a field it lacks, and inherited accessors run on the + * instance, because this is the instance itself. + */ +export function fieldsOf(instance: DataControllerInstance): RowFields { + if (!holdsFields(instance)) { + throw new TypeError("A data controller must be an object."); + } + return instance; +} + +function holdsFields(instance: DataControllerInstance): instance is RowFields { + return isObject(instance); +} + /** The object the instances of a class inherit from. */ export function prototypeOf(target: Class): object { const prototype: unknown = target.prototype; diff --git a/src/internal/read-properties.ts b/src/internal/read-properties.ts index 21721c7..ba3ad5e 100644 --- a/src/internal/read-properties.ts +++ b/src/internal/read-properties.ts @@ -6,7 +6,7 @@ import type { RowFields, } from "../metadata"; import { isFunction } from "./guards"; -import { prototypeOf } from "./prototypes"; +import { fieldsOf, prototypeOf } from "./prototypes"; export async function ReadProperties( obj: DataControllerInstance, @@ -17,7 +17,7 @@ export async function ReadProperties( ) { const readable = meta.readable[action ?? "_default"] ?? meta.readable._default; - const instance = { ...obj }; + const instance = fieldsOf({ ...obj }); const res: RowFields = {}; for (const [key, field] of readable.props) { if (onlyList && !field.listable) { diff --git a/src/internal/validation.ts b/src/internal/validation.ts index 8e3e1a7..29bfdb8 100644 --- a/src/internal/validation.ts +++ b/src/internal/validation.ts @@ -18,6 +18,7 @@ import type { DataControllerInstance, RowFields, } from "../metadata"; +import { fieldsOf } from "./prototypes"; import { isFieldValues, isObject } from "./guards"; type RequestBody = Validation.RequestBody; @@ -52,7 +53,7 @@ export function Lock( ) { for (const [modifier, field] of meta.modifierKeys.entries()) { const keyed: KeyedModifier = modifier; - const key: unknown = obj[field]; + const key = fieldsOf(obj)[field]; lock(data, keyed, written, key); } } @@ -72,7 +73,7 @@ export function Unlock( } for (const [modifier, field] of meta.modifierKeys.entries()) { const keyed: KeyedModifier = modifier; - const key: unknown = obj[field]; + const key = fieldsOf(obj)[field]; unlock(dbData, keyed, undefined, key); for (const [foreign, fieldData] of Object.entries(meta.fields)) { const related = dbData[foreign]; @@ -92,7 +93,7 @@ export function UnlockRequest( const modifiers = Array.from(meta.modifierKeys.entries()).map( ([modifier, field]) => ({ modifier, - args: [obj[field]], + args: [fieldsOf(obj)[field]], }), ); return unlockrequest(meta.tableClass, row, field, modifiers); diff --git a/src/tests/internal/controller-fields.test.ts b/src/tests/internal/controller-fields.test.ts new file mode 100644 index 0000000..d03bb44 --- /dev/null +++ b/src/tests/internal/controller-fields.test.ts @@ -0,0 +1,28 @@ +import assert from "node:assert"; + +import { fieldsOf } from "../../internal/prototypes"; + +class Holder { + own = "own value"; + + get inherited(): string { + return `read from ${this.own}`; + } +} + +describe("fieldsOf", () => { + it("answers own fields and inherited accessors by name", () => { + const fields = fieldsOf(new Holder()); + + assert.strictEqual(fields.own, "own value"); + assert.strictEqual(fields.inherited, "read from own value"); + assert.strictEqual(fields.absent, undefined); + }); + + it("refuses a value that is not an object", () => { + assert.throws( + () => fieldsOf(() => undefined), + /A data controller must be an object/, + ); + }); +});