diff --git a/packages/agent/prompts/REALIZE_OPERATION_CORRECT.md b/packages/agent/prompts/REALIZE_OPERATION_CORRECT.md index bba020e269c..4c3c77a4685 100644 --- a/packages/agent/prompts/REALIZE_OPERATION_CORRECT.md +++ b/packages/agent/prompts/REALIZE_OPERATION_CORRECT.md @@ -183,6 +183,7 @@ When TS2353 says `'X' does not exist in type 'YSelect'`: - Table name `shopping_categories` vs property name `category` - FK column `shopping_seller_id` vs relation `seller` - DTO name `orderItems` vs property name `items` + - Abbreviated FK `organization_id` vs actual column `hrm_platform_organization_id` 4. After finding the correct name, update BOTH the `select` clause AND any `transform`/return code that references the relation ### 4.6. Unwrapping Transformer.select() with `.select` @@ -302,6 +303,9 @@ export async function method__path(props: {...}): Promise { | Table name in query | Use relation property name | Check Prisma schema | | `.select().select` | Remove trailing `.select` | - | | `Prisma.DbNull` on non-Json column | Use plain `null` | `Prisma.DbNull` only for `Json?` | +| `{ equals: null }` in where | Use direct `null` | `where: { field: null }` | +| Lowercase `not`/`and`/`or` | Uppercase `NOT`/`AND`/`OR` | Prisma logical operators are UPPERCASE | +| Abbreviated FK name | Use full name from schema | `hrm_platform_organization_id`, not `organization_id` | | Type validation code | **DELETE IT** | No alternative | ## 7. Final Checklist @@ -321,6 +325,8 @@ export async function method__path(props: {...}): Promise { ### Prisma Operations - [ ] Used relation property names (NOT table names or FK columns) +- [ ] FK column names exact from schema (never abbreviated) +- [ ] `where` filters: direct `null`, uppercase `NOT`/`AND`/`OR` - [ ] `satisfies Prisma.{table}FindManyArgs` on inline nested selects - [ ] Transformer.select() assigned directly (NOT `.select().select`) - [ ] Select includes all accessed fields (relations, scalars, FK columns) diff --git a/packages/agent/prompts/REALIZE_OPERATION_WRITE.md b/packages/agent/prompts/REALIZE_OPERATION_WRITE.md index 88c47285722..453c1f57a43 100644 --- a/packages/agent/prompts/REALIZE_OPERATION_WRITE.md +++ b/packages/agent/prompts/REALIZE_OPERATION_WRITE.md @@ -458,9 +458,10 @@ const customerId = props.customer.id; // From ActorPayload **Before writing ANY query**: 1. READ the database schema thoroughly -2. VERIFY each field name (case-sensitive) +2. VERIFY each field name character-for-character (case-sensitive) 3. VERIFY relation property names from schema -4. NEVER fabricate, imagine, or guess +4. Copy FK column names exactly — never abbreviate (e.g., `hrm_platform_organization_id`, NOT `organization_id`) +5. NEVER fabricate, imagine, or guess **Key Hints from DTO Schema** — each DTO property has JSDoc annotations: - `@x-autobe-database-schema`: The DB table this DTO maps to @@ -576,7 +577,39 @@ await MyGlobal.prisma.categories.updateMany({ `Prisma.DbNull` is reserved exclusively for JSON-type columns (`Json?`). For all other nullable types, plain `null` is the correct value. -### 8.6. Data Transformation Rules +### 8.6. Prisma Where Filter Syntax + +Prisma `where` clauses have strict syntax. These are the most common mistakes: + +```typescript +// ❌ WRONG - { equals: null } for nullable filter +where: { deleted_at: { equals: null } } + +// ✅ CORRECT - Direct null comparison +where: { deleted_at: null } + +// ❌ WRONG - Lowercase `not` (TS2353) +where: { not: { status: "deleted" } } + +// ✅ CORRECT - Uppercase `NOT` for logical operators +where: { NOT: { status: "deleted" } } + +// ❌ WRONG - Nested relation filter with scalar value +where: { department: props.departmentId } + +// ✅ CORRECT - Relation filter uses object with `id` +where: { department: { id: props.departmentId } } + +// ❌ WRONG - Nullable relation filter without handling +where: { parent_department: { id: parentId } } // Fails when null + +// ✅ CORRECT - Nullable relation: use FK column directly +where: { parent_department_id: parentId ?? null } +``` + +**Prisma logical operators are UPPERCASE**: `AND`, `OR`, `NOT` — never `and`, `or`, `not`. + +### 8.7. Data Transformation Rules | Transformation | Pattern | |----------------|---------| @@ -636,7 +669,7 @@ return { }; ``` -### 8.7. DELETE Operation: Cascade Deletion +### 8.8. DELETE Operation: Cascade Deletion All tables use `onDelete: Cascade` in their foreign key relations. When deleting a record, simply delete the target row — the database automatically cascades to all dependent rows. @@ -658,7 +691,7 @@ await MyGlobal.prisma.shopping_sales.delete({ }); ``` -### 8.8. Manual CREATE Example +### 8.9. Manual CREATE Example ```typescript export async function postShoppingSaleReview(props: { @@ -856,9 +889,11 @@ throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); ### Manual Code (when no Collector/Transformer) - [ ] Verified ALL field/relation names against database schema +- [ ] FK column names copied exactly — never abbreviated (e.g., `hrm_platform_organization_id`) - [ ] Used relation property names (NOT table names or FK columns) - [ ] Used `connect` syntax for relations (NOT direct FK assignment) - [ ] `satisfies Prisma.{table}FindManyArgs` on inline nested selects +- [ ] `where` filters: direct `null` (not `{ equals: null }`), uppercase `NOT`/`AND`/`OR` - [ ] Converted dates with `.toISOString()` - [ ] Handled null→undefined for optional fields - [ ] Handled null→null for nullable fields diff --git a/packages/agent/prompts/REALIZE_TRANSFORMER_CORRECT.md b/packages/agent/prompts/REALIZE_TRANSFORMER_CORRECT.md index 3393c7943ce..38e9a60108f 100644 --- a/packages/agent/prompts/REALIZE_TRANSFORMER_CORRECT.md +++ b/packages/agent/prompts/REALIZE_TRANSFORMER_CORRECT.md @@ -256,9 +256,30 @@ export function select() { **Key rule**: Every property accessed on `input` in `transform()` MUST have a corresponding entry in `select()`. -### 5.6. FK Column Names Use `snake_case` from the Schema +### 5.6. Computed Fields Selected as Columns (TS2353) -Foreign key columns always use the exact `snake_case` name defined in the Prisma schema. The relation property name (often camelCase) is a separate concept — combining them produces a name that exists nowhere: +When TS2353 says a field doesn't exist and it looks like an aggregation (e.g., `total_hours`, `average_rating`, `total_count`), it's likely a computed field that is NOT a database column. + +```typescript +// ❌ ERROR: 'total_billable_hours' does not exist in type Select +select: { total_billable_hours: true } + +// ✅ FIX: Select the source relation, compute in transform() +select: { + timelogs: { select: { hours: true, billable: true } } + satisfies Prisma.hrm_platform_timelogsFindManyArgs, +} +// transform(): +totalBillableHours: input.timelogs + .filter(t => t.billable) + .reduce((sum, t) => sum + t.hours, 0), +``` + +**Diagnosis**: If a field name sounds like an aggregation (`*_count`, `total_*`, `average_*`), it's computed from a relation — select the relation instead. + +### 5.7. FK Column Names: Exact `snake_case` from Schema (Never Abbreviate) + +Foreign key columns always use the FULL exact `snake_case` name defined in the Prisma schema. The relation property name (often camelCase) is a separate concept — combining or abbreviating them produces a name that exists nowhere: ```typescript // Prisma schema: @@ -270,11 +291,17 @@ select: { parentComment_id: true } // ✅ CORRECT: exact column name from schema select: { parent_comment_id: true } + +// ❌ ERROR: 'organization_id' — abbreviated FK name +select: { organization_id: true } + +// ✅ CORRECT: full FK name from schema +select: { hrm_platform_organization_id: true } ``` **Compiler name suggestions are authoritative.** When the compiler says `Did you mean 'Y'?`, it is matching against the schema's actual field list — `Y` is the correct name. Adopt the suggested name for every occurrence in `select()`, `transform()`, and inline objects throughout the entire file. A single wrong name cascades into multiple errors, so one rename can resolve many diagnostics at once. -### 5.7. Typia Tag Type Mismatch +### 5.8. Typia Tag Type Mismatch ```typescript // ❌ ERROR: Type 'number & Type<"int32">' is not assignable to type 'Minimum<0>' @@ -296,7 +323,8 @@ count: input._count.reviews satisfies number as number, ### Naming - [ ] Relation property names from Prisma model (NOT table names) -- [ ] FK columns use exact `snake_case` from schema +- [ ] FK columns use exact full `snake_case` from schema (never abbreviated) +- [ ] Computed fields derived from relations in `transform()`, not selected as columns - [ ] Transformer.select() assigned directly (NOT `.select().select`) ### Neighbor Reuse diff --git a/packages/agent/prompts/REALIZE_TRANSFORMER_WRITE.md b/packages/agent/prompts/REALIZE_TRANSFORMER_WRITE.md index d77adae38da..44a94703de4 100644 --- a/packages/agent/prompts/REALIZE_TRANSFORMER_WRITE.md +++ b/packages/agent/prompts/REALIZE_TRANSFORMER_WRITE.md @@ -240,6 +240,7 @@ select: { **Rules**: 1. If you need a relation not listed in the table, the DTO field is likely a computed field (see Section 8) 2. FK columns (e.g., `reddit_clone_member_id`) are scalar fields, NOT relations — select them with `true`, not `{ select: {...} }` +3. FK column names use the FULL name from the schema — never abbreviate (e.g., `hrm_platform_organization_id`, NOT `organization_id`) ### 6.3. Mandatory Neighbor Transformer Reuse @@ -418,6 +419,17 @@ Cross-check: every `transformMappings` entry must have a corresponding line in t When a DTO field doesn't exist as a database column, select the underlying relation and compute in `transform()`. Every aggregation is backed by a real relation — select that relation, then derive the value. +**NEVER select a computed field directly** — it does not exist as a column: + +```typescript +// ❌ WRONG (TS2353) - total_hours is NOT a column, it's computed from timelogs +select: { total_hours: true } + +// ✅ CORRECT - select the source relation, compute in transform() +select: { timelogs: { select: { hours: true } } } +// transform(): total_hours = input.timelogs.reduce((sum, t) => sum + t.hours, 0) +``` + ```typescript // DTO: reviewCount, averageRating (NOT in DB) // Underlying relation: reviews (hasMany) diff --git a/packages/agent/src/orchestrate/interface/programmers/AutoBeInterfaceSchemaPropertyReviseProgrammer.ts b/packages/agent/src/orchestrate/interface/programmers/AutoBeInterfaceSchemaPropertyReviseProgrammer.ts index 4528840a0a1..04b35ec4b09 100644 --- a/packages/agent/src/orchestrate/interface/programmers/AutoBeInterfaceSchemaPropertyReviseProgrammer.ts +++ b/packages/agent/src/orchestrate/interface/programmers/AutoBeInterfaceSchemaPropertyReviseProgrammer.ts @@ -59,6 +59,28 @@ export namespace AutoBeInterfaceSchemaPropertyReviseProgrammer { `, }); + // check that at least one property survives after revisions + if ( + props.revises.length > 0 && + props.revises.every((r) => r.type === "erase") + ) + props.errors.push({ + path: `${props.path}.revises`, + expected: "At least one non-erase revision to retain a property", + value: props.revises.map((r) => r.type), + description: StringUtil.trim` + All revisions are "erase", which would leave the schema with zero + properties. An object type used as a DTO must have at least one + property — otherwise the downstream Realize stage will fail with + TypeScript compilation errors (TS2339). + + Keep at least one property by using "depict", "create", or "update" + instead of "erase" for the essential fields. + + Note that, this is not a recommendation, but an instruction you must follow. + `, + }); + if (props.model === null) return; // check all DB schema properties are revised diff --git a/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaFactory.ts b/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaFactory.ts index 162e5597dae..b5ebe6aedf8 100644 --- a/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaFactory.ts +++ b/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaFactory.ts @@ -50,6 +50,30 @@ export namespace AutoBeJsonSchemaFactory { if (value.properties.limit === undefined) value.properties.limit = pageRequest.properties.limit; } + + // Rewrite every $ref pointing to a bogus .IPagination variant + // (e.g. IEcommerceMall.IPagination) → IPage.IPagination. + for (const value of Object.values(schemas)) + AutoBeOpenApiTypeChecker.skim({ + schema: value, + accessor: "", + closure: (next) => { + if ( + AutoBeOpenApiTypeChecker.isReference(next) && + next.$ref.endsWith(".IPagination") && + next.$ref !== "#/components/schemas/IPage.IPagination" + ) + next.$ref = "#/components/schemas/IPage.IPagination"; + }, + }); + + // Delete the bogus schemas themselves so the LLM never sees them + // in subsequent iterations. Covers both entity variants + // (IEcommerceMall.IPagination) and their page wrappers + // (IPageIEcommerceMall.IPagination). + for (const key of Object.keys(schemas)) + if (key.endsWith(".IPagination") && key !== "IPage.IPagination") + delete schemas[key]; }; export const fixAuthorizationSchemas = ( diff --git a/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaValidator.ts b/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaValidator.ts index 0b2e1611bd9..cc94f20697f 100644 --- a/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaValidator.ts +++ b/packages/agent/src/orchestrate/interface/utils/AutoBeJsonSchemaValidator.ts @@ -52,6 +52,7 @@ export namespace AutoBeJsonSchemaValidator { validateReferenceId(props); validatePropertyNames(props); validateNumericRanges(props); + validatePaginationVariant(props); // validateEmptyProperties(props); vo(props.typeName, props.schema); @@ -724,6 +725,34 @@ export namespace AutoBeJsonSchemaValidator { }); }; + const validatePaginationVariant = (props: IProps): void => { + // Reject .IPagination variants on entity types (only IPage.IPagination is valid) + if ( + props.typeName.endsWith(".IPagination") && + props.typeName !== "IPage.IPagination" + ) + props.errors.push({ + path: props.path, + expected: `No .IPagination variant — only "IPage.IPagination" is valid`, + value: props.typeName, + description: StringUtil.trim` + You have defined a type ${JSON.stringify(props.typeName)} with an + ".IPagination" suffix, but ".IPagination" is NOT a valid DTO variant. + + The only valid pagination metadata type is "IPage.IPagination", which is + a system preset containing { current, limit, records, pages }. + + Common DTO variants include: .ISummary, .ICreate, .IUpdate, .IRequest, + .IInvert, .IJoin, .ILogin, .IAuthorized. + + Remove this type entirely. If you need pagination support, the system + automatically wraps your entity ISummary types in IPage wrappers. + + Note that, this is not a recommendation, but an instruction you must follow. + `, + }); + }; + // const validateEmptyProperties = (props: IProps): void => { // if (AutoBeOpenApiTypeChecker.isObject(props.schema) === false) return; // if (Object.keys(props.schema.properties).length !== 0) return;