Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ export namespace Validation {
return Array.isArray(value) && value.length === 0;
}

const isAbsentFromBody = (body: RequestBody, name: string) => !(name in body);
const isAbsentFromBody = (body: RequestBody, name: string) =>
!Object.hasOwn(body, name);

const isClearedByBody = (body: RequestBody, name: string) =>
Object.hasOwn(body, name) && isClearingValue(body[name]);
Expand Down Expand Up @@ -563,7 +564,7 @@ export namespace Validation {
const parsed = { ...obj };
const invalid: string[] = [];
for (const [name, field] of Object.entries(meta.fields)) {
if (!field.validator || !(name in obj)) continue;
if (!field.validator || !Object.hasOwn(obj, name)) continue;
const result = toParseResult(await field.validator(obj[name]), obj[name]);
if (result.success) {
parsed[name] = result.data;
Expand Down
8 changes: 5 additions & 3 deletions src/internal/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { Computed } from "./computed";
import { wrapDeferredJoined } from "./deferred-joined-stream";
import { Foreign } from "./foreign";
import { ownEntry } from "./meta-records";
import {
deferredJoinedFields,
Joined,
Expand Down Expand Up @@ -77,9 +78,10 @@ function isHeldByKeyedModifier(meta: DataAPIMeta, field: string): boolean {
Array.from(meta.modifierKeys.keys(), (modifier) => modifier.name),
);
return (
getMetadata(meta.tableClass, ModifiersStaticMetadata).fields[field]?.some(
({ id }) => keyed.has(id),
) ?? false
ownEntry(
getMetadata(meta.tableClass, ModifiersStaticMetadata).fields,
field,
)?.some(({ id }) => keyed.has(id)) ?? false
);
}

Expand Down
13 changes: 8 additions & 5 deletions src/internal/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
RowFields,
WritableAccessFields,
} from "../metadata";
import { ownEntry } from "./meta-records";
import { fieldsOf, prototypeOf } from "./prototypes";

function writableInBody(
Expand All @@ -28,8 +29,9 @@ function writeTargetDefaults(
for (const [key, value] of defaults) {
if (value !== undefined) {
instance[key] = value;
if (key in meta.fields) {
dbData[meta.fields[key].dbName || key] = value;
const field = ownEntry(meta.fields, key);
if (field) {
dbData[field.dbName || key] = value;
}
}
}
Expand All @@ -54,13 +56,14 @@ export function WriteProperties(
writeTargetDefaults(instance, meta, dbData);
}
for (const [key, field] of writable.props) {
instance[key] = bodyData[key];
dbData[field.dbName || key] = bodyData[key];
const value = ownEntry(bodyData, key);
instance[key] = value;
dbData[field.dbName || key] = value;
}
instance.table = dbData;
Object.setPrototypeOf(instance, prototypeOf(meta.target));
for (const [key, field] of writable.setters) {
field.desc?.set?.apply(instance, [bodyData[key]]);
field.desc?.set?.apply(instance, [ownEntry(bodyData, key)]);
}
return dbData;
}
108 changes: 108 additions & 0 deletions src/tests/components/prototype_named_field.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { expect } from "chai";
import { Controller } from "@antelopejs/interface-api";
import { Schema } from "@antelopejs/interface-database";
import {
DataController,
DefaultRoutes,
RegisterDataController,
} from "../../index";
import { Query, Validation } from "../../components";
import {
Access,
AccessMode,
DataAPIMeta,
Mandatory,
ModelReference,
Validator,
} from "../../metadata";
import {
BasicDataModel,
Field,
Model,
RegisterSchema,
RegisterTable,
Table,
} from "@antelopejs/interface-database-decorators";

import { createdRowSchema, readJson } from "../internal/http";
import { newRequest } from "../utils";
import { isString } from "../../internal/guards";

const tableName = "prototype-named-field-rows";
const schemaName = "default";

@RegisterTable(tableName, schemaName)
class Row extends Table {
declare _id: string;

@Field("string")
declare title: string;
}
Field("string")(Row.prototype, "toString");
class RowModel extends BasicDataModel(Row, tableName) {}

async function createController(
testName: string,
toStringDecorators: PropertyDecorator[],
) {
@RegisterDataController()
class _PrototypeNamedFieldAPI extends DataController(
Row,
{ new: DefaultRoutes.New },
Controller(`/${testName}`),
) {
@ModelReference()
@Model(RowModel)
declare rowModel: RowModel;

@Access(AccessMode.ReadWrite)
declare title: string;
}
Access(AccessMode.ReadWrite)(_PrototypeNamedFieldAPI.prototype, "toString");
for (const decorate of toStringDecorators) {
decorate(_PrototypeNamedFieldAPI.prototype, "toString");
}

await RegisterSchema(schemaName);
await Schema.get(schemaName)?.instance().table(tableName).delete();
}

describe("New route - a field named like an Object.prototype member", () => {
it("reports the field missing when it is mandatory and the body omits it", async () => {
await createController("tostring-mandatory", [Mandatory("new")]);

const response = await newRequest("tostring-mandatory", { title: "a" });

expect(response.status, await response.clone().text()).to.equal(400);
expect(await response.json()).to.deep.equal({
code: Validation.MISSING_MANDATORY_FIELDS,
fields: ["toString"],
});
});

it("skips the field's validator when the body omits it", async () => {
await createController("tostring-validated", [Validator(isString)]);

const response = await newRequest("tostring-validated", { title: "a" });

expect(response.status, await response.clone().text()).to.equal(200);
expect((await readJson(createdRowSchema, response))._id).to.be.a("string");
});
});

describe("Query.WriteProperties - a target default named like an Object.prototype member", () => {
it("writes only declared fields", async () => {
class Defaults {
valueOf = "default";
other = "x";
title = "t";
}
const meta = new DataAPIMeta(Defaults);
meta.tableClass = class TableRow {};
meta.setMode("title", AccessMode.ReadWrite);

const dbData = await Query.WriteProperties({}, meta, {});

expect(Object.keys(dbData)).to.eql(["title"]);
});
});
17 changes: 17 additions & 0 deletions src/tests/components/prototype_named_filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Filter,
Listable,
ModelReference,
Sortable,
} from "../../metadata";
import {
BasicDataModel,
Expand Down Expand Up @@ -60,6 +61,7 @@ async function createController(testName: string) {
Listable()(_PrototypeNamedFilterAPI.prototype, "valueOf");
Access(AccessMode.ReadOnly)(_PrototypeNamedFilterAPI.prototype, "valueOf");
Filter(undefined, false)(_PrototypeNamedFilterAPI.prototype, "valueOf");
Sortable()(_PrototypeNamedFilterAPI.prototype, "valueOf");

await RegisterSchema(schemaName);
const schema = Schema.get(schemaName);
Expand Down Expand Up @@ -97,4 +99,19 @@ describe("List route - a field named like an Object.prototype member", () => {
const data = await readJson(listSchema, response);
expect(data.results.map(({ title }) => title)).to.eql(["alpha"]);
});

it("sorts on the field", async () => {
await createController("valueof-sorted");

const response = await listRequest("valueof-sorted", {
sortKey: "valueOf",
});

expect(response.status, await response.clone().text()).to.equal(200);
const data = await readJson(listSchema, response);
expect(data.results.map(({ title }) => title)).to.have.members([
"alpha",
"beta",
]);
});
});
Loading