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
Original file line number Diff line number Diff line change
Expand Up @@ -372,12 +372,18 @@ function renderColumn(
}
}

// @autoSet fields: emit .$defaultFn(() => new Date().toISOString()) so Drizzle
// inserts stamp the server-side timestamp automatically. This means callers don't
// need to supply createdAt / updatedAt in INSERT calls — Drizzle fills them in.
// @autoSet fields: emit a $defaultFn so Drizzle inserts stamp the server-side timestamp
// automatically. This means callers don't need to supply createdAt / updatedAt in INSERT
// calls — Drizzle fills them in. The stamp's shape must match ctx.timestampMode — the base
// column type already does (mapColumnType, above) — or a "date" column ends up with a
// $defaultFn that hands Drizzle a string, which fails to typecheck (reported against an
// adopting project: TS2322 "Type 'string' is not assignable to type 'Date | SQL<unknown>'",
// cascading into every generated insert/update query touching the field).
const autoSet = field.attr(FIELD_ATTR_AUTO_SET);
const autoSetSuffix = (autoSet === "onCreate" || autoSet === "onUpdate")
? `.$defaultFn(() => new Date().toISOString())`
? ctx.timestampMode === "date"
? `.$defaultFn(() => new Date())`
: `.$defaultFn(() => new Date().toISOString())`
: "";

// $type<E[]>() chain — emitted as Code (not a string modifier) so ts-poet can
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ export function renderInsertSchemaOnly(obj: MetaObject, ctx?: RenderContext): Co

if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
insertFieldLines.push(
code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
ctx?.timestampMode === "date"
? code` ${child.name}: z.date().optional().transform(() => new Date())`
: code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
);
} else {
insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child, obj, ctx)}`);
Expand Down Expand Up @@ -338,7 +340,9 @@ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code
// Insert schema: @autoSet fields use transform (always override client input).
if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
insertFieldLines.push(
code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
ctx?.timestampMode === "date"
? code` ${child.name}: z.date().optional().transform(() => new Date())`
: code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
);
// Preserving schema: the @autoSet column is validated verbatim (its natural
// field expr) so an import/restore keeps the caller's original timestamp.
Expand All @@ -354,7 +358,9 @@ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code
// Omit: creation timestamps cannot be changed after creation
} else if (autoSet === AUTO_SET_ON_UPDATE) {
updateFieldLines.push(
code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
ctx?.timestampMode === "date"
? code` ${child.name}: z.date().optional().transform(() => new Date())`
: code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
);
} else {
// All non-autoSet fields are optional in the update schema (PATCH semantics).
Expand Down Expand Up @@ -511,8 +517,14 @@ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext)
break;
case FIELD_SUBTYPE_DATE:
case FIELD_SUBTYPE_TIME:
baseStr = "z.string()"; // calendar date / time-of-day — always ISO-string-shaped, not governed by timestampMode
break;
case FIELD_SUBTYPE_TIMESTAMP:
baseStr = "z.string()";
// Must agree with column-mapper.ts's mapColumnType, which already honors
// ctx.timestampMode for the Drizzle column itself — z.string() here regardless would
// disagree with a "date"-mode column (Date-typed) and fail to typecheck downstream
// (reported against an adopting project).
baseStr = ctx?.timestampMode === "date" ? "z.date()" : "z.string()";
break;
case FIELD_SUBTYPE_ENUM: {
const values = enumValues(field);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,52 @@ describe("renderDrizzleSchema — Postgres", () => {
expect(out).toContain("varchar(\"title\", { length: 200 }).notNull()");
});

// Reported against an adopting project: @autoSet's $defaultFn ignored timestampMode and always
// returned a string, failing to typecheck against a "date"-mode column (Date-typed).
test("@autoSet field respects timestampMode: \"date\" — $defaultFn returns Date, not a string", () => {
const post = makePost();
const createdAt = metaField(FIELD_SUBTYPE_TIMESTAMP, "createdAt");
createdAt.setAttr("required", true);
createdAt.setAttr("autoSet", "onCreate");
post.addChild(createdAt);

const root = makeRoot([post]);
const ctx = makeRenderContext({
dialect: "postgres",
timestampMode: "date",
loadedRoot: root,
outDir: "/x",
dbImport: "~/db",
pkMap: buildPkMap(root),
relationMap: buildRelationMap(root),
});
const out = renderDrizzleSchema(root.findObject("Post")!, ctx).toString();
expect(out).toContain(".$defaultFn(() => new Date())");
expect(out).not.toContain(".toISOString()");
expect(out).toMatch(/timestamp\("created_at",\s*\{\s*mode:\s*"date"/);
});

test("@autoSet field defaults to timestampMode: \"string\" unchanged (no config set)", () => {
const post = makePost();
const createdAt = metaField(FIELD_SUBTYPE_TIMESTAMP, "createdAt");
createdAt.setAttr("required", true);
createdAt.setAttr("autoSet", "onCreate");
post.addChild(createdAt);

const root = makeRoot([post]);
const ctx = makeRenderContext({
dialect: "postgres",
loadedRoot: root,
outDir: "/x",
dbImport: "~/db",
pkMap: buildPkMap(root),
relationMap: buildRelationMap(root),
});
const out = renderDrizzleSchema(root.findObject("Post")!, ctx).toString();
expect(out).toContain(".$defaultFn(() =>");
expect(out).toContain("new Date().toISOString()");
});

test("Postgres long PK emits bigserial, not serial", () => {
const root = makeRoot([makePost()]);
const ctx = makeRenderContext({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,49 @@ import { TypeId, TYPE_IDENTITY, TYPE_VALIDATOR,
IDENTITY_SUBTYPE_PRIMARY, OBJECT_SUBTYPE_ENTITY,
VALIDATOR_SUBTYPE_REGEX, VALIDATOR_SUBTYPE_LENGTH,
MetaDataLoader, InMemoryStringSource } from "@metaobjectsdev/metadata";
import { meta, metaObject, metaField } from "../_meta-build.js";
import { meta, metaObject, metaField, metaRoot } from "../_meta-build.js";
import { renderZodValidators } from "../../src/templates/zod-validators.js";
import { makeRenderContext } from "../../src/render-context.js";
import { buildPkMap } from "../../src/pk-resolver.js";
import { buildRelationMap } from "../../src/relation-resolver.js";
import { FIELD_SUBTYPE_TIMESTAMP } from "@metaobjectsdev/metadata";

describe("renderZodValidators", () => {
// Reported against an adopting project: a plain field.timestamp (autoSet or not) always got
// z.string() regardless of timestampMode, disagreeing with a "date"-mode Drizzle column
// (Date-typed) and failing to typecheck downstream.
test("timestampMode: \"date\" — field.timestamp gets z.date(), @autoSet gets a Date-returning transform", () => {
const post = metaObject(OBJECT_SUBTYPE_ENTITY, "Post");
const id = metaField(FIELD_SUBTYPE_LONG, "id");
post.addChild(id);
const updatedAt = metaField(FIELD_SUBTYPE_TIMESTAMP, "updatedAt"); // plain, not autoSet
post.addChild(updatedAt);
const createdAt = metaField(FIELD_SUBTYPE_TIMESTAMP, "createdAt");
createdAt.setAttr("required", true);
createdAt.setAttr("autoSet", "onCreate");
post.addChild(createdAt);
const primary = meta(new TypeId(TYPE_IDENTITY, IDENTITY_SUBTYPE_PRIMARY), "primary");
primary.setAttr("fields", ["id"]);
primary.setAttr("generation", "increment");
post.addChild(primary);

const root = metaRoot();
root.addChild(post);
const ctx = makeRenderContext({
dialect: "postgres",
timestampMode: "date",
loadedRoot: root,
outDir: "/x",
dbImport: "~/db",
pkMap: buildPkMap(root),
relationMap: buildRelationMap(root),
});
const out = renderZodValidators(post, ctx).toString();
expect(out).toContain("updatedAt: z.date()"); // plain field, general zodFieldExpr path
expect(out).toContain("z.date().optional().transform(() =>"); // @autoSet insert path
expect(out).not.toContain("z.string()"); // no stale string-typed timestamp anywhere
expect(out).not.toContain(".toISOString()");
});
test("emits InsertSchema with required fields and optional unset fields", () => {
const post = metaObject(OBJECT_SUBTYPE_ENTITY, "Post");
const id = metaField(FIELD_SUBTYPE_LONG, "id");
Expand Down
Loading