Skip to content

Commit 1e93499

Browse files
dmealingclaude
andcommitted
refactor(migrate-ts): post-review simplifications in referential-actions
- Extract isRequired, findField, readIdentityFields to referential-actions.ts and remove character-for-character duplicates from expected-schema.ts - Collapse makeDoc in referential-actions.test.ts to delegate to the existing weekDoc factory (same document structure, one fewer function) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d0a624c commit 1e93499

3 files changed

Lines changed: 40 additions & 80 deletions

File tree

server/typescript/packages/migrate-ts/src/expected-schema.ts

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import type { MetaData, MetaObject, MetaReferenceIdentity, MetaRoot } from "@metaobjectsdev/metadata";
1+
import type { MetaData, MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
22
import {
33
TYPE_OBJECT,
4-
TYPE_VALIDATOR,
54
MetaSource,
6-
VALIDATOR_SUBTYPE_REQUIRED,
7-
IDENTITY_ATTR_FIELDS,
85
IDENTITY_ATTR_GENERATION,
96
IDENTITY_ATTR_UNIQUE,
10-
FIELD_ATTR_REQUIRED,
117
FIELD_ATTR_DEFAULT,
128
FIELD_ATTR_UNIQUE,
139
FIELD_SUBTYPE_STRING,
@@ -34,7 +30,13 @@ import type { SqlType } from "./sql-type.js";
3430
import type {
3531
SchemaSnapshot, TableDescriptor, ColumnDescriptor, IndexDescriptor, FkDescriptor,
3632
} from "./types.js";
37-
import { resolveReferentialActions, validateSetNullNullability } from "./referential-actions.js";
33+
import {
34+
resolveReferentialActions,
35+
validateSetNullNullability,
36+
readIdentityFields,
37+
findField,
38+
isRequired,
39+
} from "./referential-actions.js";
3840

3941
export interface BuildExpectedSchemaOptions {
4042
/**
@@ -297,19 +299,14 @@ function buildColumn(
297299
isPk: boolean,
298300
pkGeneration: string | undefined,
299301
): ColumnDescriptor {
300-
const requiredAttr = field.ownAttr(FIELD_ATTR_REQUIRED);
301-
// BaseEntity-style metadata expresses required via a validator.required child;
302-
// direct attr form is the alternative. Either signals NOT NULL.
303-
const hasRequiredValidator = field.ownChildren().some(
304-
(c) => c.type === TYPE_VALIDATOR && c.subType === VALIDATOR_SUBTYPE_REQUIRED,
305-
);
306-
const isRequired = requiredAttr === true || requiredAttr === "true" || hasRequiredValidator;
302+
// Both the @required attr and the validator.required child signal NOT NULL.
303+
const fieldIsRequired = isRequired(field);
307304
const defaultRaw = field.ownAttr(FIELD_ATTR_DEFAULT);
308305

309306
const col: ColumnDescriptor = {
310307
name: resolveColumnName(field),
311308
sqlType: subtypeToSqlType(field.subType),
312-
nullable: !isPk && !isRequired,
309+
nullable: !isPk && !fieldIsRequired,
313310
};
314311

315312
if (typeof defaultRaw === "string" && defaultRaw.length > 0) {
@@ -347,25 +344,6 @@ function subtypeToSqlType(subType: string): SqlType {
347344
}
348345
}
349346

350-
function readIdentityFields(identity: MetaData): string[] {
351-
const raw = identity.ownAttr(IDENTITY_ATTR_FIELDS);
352-
if (Array.isArray(raw)) {
353-
return raw.map(String).filter((s) => s.length > 0);
354-
}
355-
if (typeof raw === "string") {
356-
// Fallback: comma-separated string form (defensive; canonical form is array)
357-
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
358-
}
359-
return [];
360-
}
361-
362-
function findField(entity: MetaObject, name: string): MetaData | undefined {
363-
for (const field of entity.fields()) {
364-
if (field.name === name) return field;
365-
}
366-
return undefined;
367-
}
368-
369347
function toSnake(s: string): string {
370348
return s
371349
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")

server/typescript/packages/migrate-ts/src/referential-actions.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@ import {
1212
import type { FkAction } from "./types.js";
1313
import { SetNullNotNullableError } from "./errors.js";
1414

15+
// ---------------------------------------------------------------------------
16+
// Shared field helpers — exported for use by expected-schema.ts
17+
// ---------------------------------------------------------------------------
18+
19+
export function readIdentityFields(identity: MetaData): string[] {
20+
const raw = identity.ownAttr(IDENTITY_ATTR_FIELDS);
21+
if (Array.isArray(raw)) return raw.map(String).filter((s) => s.length > 0);
22+
// Fallback: comma-separated string form (defensive; canonical form is array)
23+
if (typeof raw === "string") return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
24+
return [];
25+
}
26+
27+
export function findField(entity: MetaObject, name: string): MetaData | undefined {
28+
for (const field of entity.fields()) {
29+
if (field.name === name) return field;
30+
}
31+
return undefined;
32+
}
33+
34+
export function isRequired(field: MetaData): boolean {
35+
const attr = field.ownAttr(FIELD_ATTR_REQUIRED);
36+
if (attr === true || attr === "true") return true;
37+
return field.ownChildren().some(
38+
(c) => c.type === TYPE_VALIDATOR && c.subType === VALIDATOR_SUBTYPE_REQUIRED,
39+
);
40+
}
41+
1542
/**
1643
* Resolve the referential actions for a foreign key inferred from an
1744
* identity.reference, by correlating it with a sibling relationship on the
@@ -92,7 +119,7 @@ export function validateSetNullNullability(
92119
): void {
93120
if (onDelete !== "set-null") return;
94121

95-
const fkFieldJsNames = readRefFields(ref);
122+
const fkFieldJsNames = readIdentityFields(ref);
96123
const offending: string[] = [];
97124
for (const jsName of fkFieldJsNames) {
98125
const field = findField(entity, jsName);
@@ -105,25 +132,3 @@ export function validateSetNullNullability(
105132
throw new SetNullNotNullableError(entity.name, constraintName, offending);
106133
}
107134
}
108-
109-
function readRefFields(ref: MetaReferenceIdentity): string[] {
110-
const raw = ref.ownAttr(IDENTITY_ATTR_FIELDS);
111-
if (Array.isArray(raw)) return raw.map(String).filter((s) => s.length > 0);
112-
if (typeof raw === "string") return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
113-
return [];
114-
}
115-
116-
function findField(entity: MetaObject, name: string): MetaData | undefined {
117-
for (const field of entity.fields()) {
118-
if (field.name === name) return field;
119-
}
120-
return undefined;
121-
}
122-
123-
function isRequired(field: MetaData): boolean {
124-
const attr = field.ownAttr(FIELD_ATTR_REQUIRED);
125-
if (attr === true || attr === "true") return true;
126-
return field.ownChildren().some(
127-
(c) => c.type === TYPE_VALIDATOR && c.subType === VALIDATOR_SUBTYPE_REQUIRED,
128-
);
129-
}

server/typescript/packages/migrate-ts/test/unit/referential-actions.test.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,31 +91,8 @@ describe("resolveReferentialActions", () => {
9191

9292
const EMPTY_SCHEMA = { tables: [], views: [] };
9393

94-
function makeDoc(rel: Record<string, unknown>) {
95-
return {
96-
"metadata.root": {
97-
package: "acme",
98-
children: [
99-
{ "object.entity": { name: "Program", children: [
100-
{ "field.long": { name: "id" } },
101-
{ "identity.primary": { "@fields": "id" } },
102-
] } },
103-
{ "object.entity": { name: "Week", children: [
104-
{ "field.long": { name: "id" } },
105-
{ "field.long": { name: "programId" } },
106-
rel,
107-
{ "identity.reference": { name: "ref_program", "@fields": ["programId"], "@references": "Program" } },
108-
{ "identity.primary": { "@fields": "id" } },
109-
] } },
110-
],
111-
},
112-
};
113-
}
114-
11594
async function buildSnapshotForRel(rel: Record<string, unknown>) {
116-
const { root, errors } = await new MetaDataLoader().load([
117-
new InMemorySource(JSON.stringify(makeDoc(rel))),
118-
]);
95+
const { root, errors } = await loadDoc(weekDoc(rel));
11996
expect(errors).toHaveLength(0);
12097
return buildExpectedSchema(root);
12198
}

0 commit comments

Comments
 (0)