From 539daef831d8f9bfcfb4d8df68c737dfa4f08917 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Dec 2025 06:14:31 +0000 Subject: [PATCH 1/8] feat: comprehensive integration test schema and bug fixes - Added exhaustive integration test schema covering all Prisma features: - All scalar types (String, Int, Float, Boolean, DateTime, Json, BigInt, Decimal, Bytes) - All array types with proper json() mapping - 5 different enums with @map support - All relationship types (1:1, 1:N, N:N implicit/explicit) - Self-referential relationships (parent/children, social follow, blocked users) - Composite primary keys (@@id) and composite foreign keys - Field mapping (@map) and table mapping (@@map) - Various @default configurations - Native PostgreSQL types (@db.*) - Edge cases (reserved words, long names, minimal models) - Model exclusion via excludeTables config - Fixed import generation to correctly extract base types for generics (enumeration() -> enumeration, json() -> json) - Fixed self-referential implicit many-to-many relationships not creating join tables (localeCompare returns 0 when model names are identical) The integration schema is now 990+ lines with 55+ models demonstrating comprehensive coverage of Prisma schema features. --- integration/generated/zero/schema.ts | 1412 +++++++++++++++++++++++++- integration/schema.prisma | 985 +++++++++++++++++- src/generators/code-generator.ts | 6 +- src/mappers/schema-mapper.ts | 9 +- tests/generator.test.ts | 8 +- 5 files changed, 2388 insertions(+), 32 deletions(-) diff --git a/integration/generated/zero/schema.ts b/integration/generated/zero/schema.ts index 2f65199..e25a622 100644 --- a/integration/generated/zero/schema.ts +++ b/integration/generated/zero/schema.ts @@ -6,52 +6,1436 @@ import { createBuilder, createCRUDBuilder, createSchema, + enumeration, + json, number, relationships, string, table, } from '@rocicorp/zero'; -export const userTable = table('User') +export type Role = 'USER' | 'ADMIN' | 'MODERATOR' | 'SUPER_ADMIN'; + +export type Status = 'pending' | 'active' | 'inactive' | 'archived' | 'deleted'; + +export type OrderStatus = + | 'DRAFT' + | 'SUBMITTED' + | 'PROCESSING' + | 'SHIPPED' + | 'DELIVERED' + | 'CANCELLED' + | 'REFUNDED'; + +export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; + +export type NotificationType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP' | 'WEBHOOK'; + +export const allScalarTypesTable = table('allScalarTypes') + .from('AllScalarTypes') + .columns({ + id: string(), + stringField: string(), + intField: number(), + floatField: number(), + booleanField: boolean(), + dateTimeField: number(), + jsonField: json(), + bigIntField: number(), + decimalField: number(), + bytesField: string(), + optionalString: string().optional(), + optionalInt: number().optional(), + optionalFloat: number().optional(), + optionalBoolean: boolean().optional(), + optionalDateTime: number().optional(), + optionalJson: json().optional(), + optionalBigInt: number().optional(), + optionalDecimal: number().optional(), + optionalBytes: string().optional(), + createdAt: number(), + updatedAt: number(), + }) + .primaryKey('id'); + +export const allArrayTypesTable = table('allArrayTypes') + .from('AllArrayTypes') + .columns({ + id: string(), + stringArray: json(), + intArray: json(), + floatArray: json(), + booleanArray: json(), + dateTimeArray: json(), + jsonArray: json(), + bigIntArray: json(), + decimalArray: json(), + bytesArray: json(), + roles: json(), + statuses: json(), + notificationTypes: json(), + createdAt: number(), + }) + .primaryKey('id'); + +export const defaultValuesTable = table('defaultValues') + .from('DefaultValues') + .columns({ + id: string(), + sequenceNum: number(), + cuidField: string(), + defaultString: string(), + defaultInt: number(), + defaultFloat: number(), + defaultBool: boolean(), + defaultTrue: boolean(), + createdAt: number(), + updatedAt: number(), + status: enumeration(), + role: enumeration(), + metadata: json(), + settings: json(), + tags: json(), + }) + .primaryKey('id'); + +export const fieldMappingTable = table('fieldMapping') + .from('FieldMapping') .columns({ id: string(), + firstName: string().from('first_name'), + lastName: string().from('last_name'), + emailAddr: string().from('email_address'), + phoneNumber: string().from('phone_number'), + createdAt: number().from('created_at'), + updatedAt: number().from('updated_at'), + deletedAt: number().from('deleted_at').optional(), + isActive: boolean().from('is_active'), + isVerified: boolean().from('is_verified'), + organizationId: string().from('organization_id'), + }) + .primaryKey('id'); + +export const userAccountTable = table('userAccounts') + .from('user_accounts') + .columns({ + id: string(), + username: string(), email: string(), + createdAt: number(), + }) + .primaryKey('id'); + +export const userProfileTable = table('userProfiles') + .from('user_profiles') + .columns({ + id: string(), + bio: string().optional(), + avatarUrl: string().from('avatar_url').optional(), + website: string().optional(), + userId: string().from('user_id'), + }) + .primaryKey('id'); + +export const userSessionTable = table('userSessions') + .from('user_sessions') + .columns({ + id: string(), + token: string(), + userAgent: string().from('user_agent').optional(), + ipAddress: string().from('ip_address').optional(), + lastActiveAt: number().from('last_active_at'), + expiresAt: number().from('expires_at'), + userId: string().from('user_id'), + }) + .primaryKey('id'); + +export const tenantUserTable = table('tenantUser') + .from('TenantUser') + .columns({ + tenantId: string(), + userId: string(), + role: enumeration(), + joinedAt: number(), + }) + .primaryKey('tenantId', 'userId'); + +export const tenantApiKeyTable = table('tenantApiKeys') + .from('tenant_api_keys') + .columns({ + tenantId: string(), + keyId: string(), name: string(), + hashedKey: string().from('hashed_key'), + permissions: json(), + expiresAt: number().from('expires_at').optional(), + createdAt: number().from('created_at'), + }) + .primaryKey('tenantId', 'keyId'); + +export const tenantTable = table('tenant') + .from('Tenant') + .columns({ + id: string(), + name: string(), + slug: string(), createdAt: number(), }) .primaryKey('id'); -export const postTable = table('Post') +export const uniqueConstraintsTable = table('uniqueConstraints') + .from('unique_constraints') .columns({ id: string(), + email: string(), + username: string(), + orgId: string().from('org_id'), + employeeId: string().from('employee_id'), + firstName: string().from('first_name'), + lastName: string().from('last_name'), + }) + .primaryKey('id'); + +export const indexedModelTable = table('indexedModels') + .from('indexed_models') + .columns({ + id: string(), + email: string(), + status: enumeration(), + createdAt: number().from('created_at'), + updatedAt: number().from('updated_at'), + categoryId: string().from('category_id'), + authorId: string().from('author_id'), title: string(), content: string().optional(), - published: boolean(), - authorId: string(), }) .primaryKey('id'); -export const userTableRelationships = relationships(userTable, ({many}) => ({ - posts: many({ +export const personTable = table('person') + .from('Person') + .columns({ + id: string(), + email: string(), + name: string(), + createdAt: number(), + spouseId: string().optional(), + }) + .primaryKey('id'); + +export const passportTable = table('passport') + .from('Passport') + .columns({ + id: string(), + passportNumber: string().from('passport_number'), + country: string(), + expiryDate: number().from('expiry_date'), + personId: string().from('person_id'), + }) + .primaryKey('id'); + +export const driverLicenseTable = table('driverLicenses') + .from('driver_licenses') + .columns({ + id: string(), + licenseNumber: string().from('license_number'), + state: string(), + expiryDate: number().from('expiry_date'), + class: string(), + personId: string().from('person_id'), + }) + .primaryKey('id'); + +export const organizationTable = table('organization') + .from('Organization') + .columns({ + id: string(), + name: string(), + slug: string(), + description: string().optional(), + website: string().optional(), + createdAt: number(), + }) + .primaryKey('id'); + +export const departmentTable = table('department') + .from('Department') + .columns({ + id: string(), + name: string(), + code: string(), + description: string().optional(), + budget: number().optional(), + organizationId: string().from('organization_id'), + }) + .primaryKey('id'); + +export const employeeTable = table('employee') + .from('Employee') + .columns({ + id: string(), + employeeNo: string().from('employee_no'), + firstName: string().from('first_name'), + lastName: string().from('last_name'), + email: string(), + title: string().optional(), + salary: number().optional(), + hireDate: number().from('hire_date'), + organizationId: string().from('organization_id'), + departmentId: string().from('department_id').optional(), + }) + .primaryKey('id'); + +export const teamTable = table('team') + .from('Team') + .columns({ + id: string(), + name: string(), + description: string().optional(), + createdAt: number(), + departmentId: string().from('department_id'), + managerId: string().from('manager_id').optional(), + }) + .primaryKey('id'); + +export const teamMemberTable = table('teamMembers') + .from('team_members') + .columns({ + id: string(), + role: string(), + joinedAt: number().from('joined_at'), + teamId: string().from('team_id'), + employeeId: string().from('employee_id'), + }) + .primaryKey('id'); + +export const projectTable = table('project') + .from('Project') + .columns({ + id: string(), + name: string(), + description: string().optional(), + status: enumeration(), + priority: enumeration(), + startDate: number().from('start_date').optional(), + endDate: number().from('end_date').optional(), + budget: number().optional(), + createdAt: number(), + organizationId: string().from('organization_id'), + }) + .primaryKey('id'); + +export const tagTable = table('tag') + .from('Tag') + .columns({ + id: string(), + name: string(), + color: string(), + createdAt: number(), + }) + .primaryKey('id'); + +export const articleTable = table('article') + .from('Article') + .columns({ + id: string(), + title: string(), + slug: string(), + content: string(), + excerpt: string().optional(), + publishedAt: number().from('published_at').optional(), + createdAt: number(), + updatedAt: number(), + }) + .primaryKey('id'); + +export const categoryTable = table('category') + .from('Category') + .columns({ + id: string(), + name: string(), + slug: string(), + description: string().optional(), + parentId: string().from('parent_id').optional(), + }) + .primaryKey('id'); + +export const skillTable = table('skill') + .from('Skill') + .columns({ + id: string(), + name: string(), + description: string().optional(), + category: string().optional(), + }) + .primaryKey('id'); + +export const workerTable = table('worker') + .from('Worker') + .columns({ + id: string(), + name: string(), + email: string(), + hourlyRate: number().from('hourly_rate'), + }) + .primaryKey('id'); + +export const workerSkillTable = table('workerSkills') + .from('worker_skills') + .columns({ + id: string(), + proficiency: number(), + yearsExperience: number().from('years_experience'), + certified: boolean(), + certifiedAt: number().from('certified_at').optional(), + workerId: string().from('worker_id'), + skillId: string().from('skill_id'), + }) + .primaryKey('id'); + +export const commentTable = table('comment') + .from('Comment') + .columns({ + id: string(), + content: string(), + createdAt: number(), + updatedAt: number(), + articleId: string().from('article_id'), + parentId: string().from('parent_id').optional(), + }) + .primaryKey('id'); + +export const treeNodeTable = table('treeNodes') + .from('tree_nodes') + .columns({ + id: string(), + name: string(), + level: number(), + path: string(), + createdAt: number(), + parentId: string().from('parent_id').optional(), + }) + .primaryKey('id'); + +export const socialUserTable = table('socialUsers') + .from('social_users') + .columns({ + id: string(), + username: string(), + name: string(), + bio: string().optional(), + createdAt: number(), + }) + .primaryKey('id'); + +export const followTable = table('follow') + .from('Follow') + .columns({ + id: string(), + followedAt: number().from('followed_at'), + followerId: string().from('follower_id'), + followingId: string().from('following_id'), + }) + .primaryKey('id'); + +export const compositePkParentTable = table('compositePkParents') + .from('composite_pk_parents') + .columns({ + orgId: string(), + recordId: string(), + name: string(), + createdAt: number(), + }) + .primaryKey('orgId', 'recordId'); + +export const compositeFkChildTable = table('compositeFkChildren') + .from('composite_fk_children') + .columns({ + id: string(), + name: string(), + value: number(), + createdAt: number(), + parentOrgId: string().from('parent_org_id'), + parentRecordId: string().from('parent_record_id'), + }) + .primaryKey('id'); + +export const taskTable = table('task') + .from('Task') + .columns({ + id: string(), + title: string(), + description: string().optional(), + status: enumeration(), + priority: enumeration(), + dueDate: number().from('due_date').optional(), + completedAt: number().from('completed_at').optional(), + createdAt: number(), + updatedAt: number(), + projectId: string().from('project_id'), + assigneeId: string().from('assignee_id').optional(), + creatorId: string().from('creator_id'), + }) + .primaryKey('id'); + +export const nativeTypesTable = table('nativeTypes') + .from('native_types') + .columns({ + id: string(), + varcharField: string(), + charField: string(), + textField: string(), + smallIntField: number(), + integerField: number(), + bigIntField: number(), + realField: number(), + doublePrecision: number(), + decimalField: number(), + moneyField: number(), + timestampField: number(), + timestampTz: number(), + dateField: number(), + timeField: number(), + timeTzField: number(), + boolField: boolean(), + byteaField: string(), + jsonField: json(), + jsonbField: json(), + uuidField: string(), + xmlField: string(), + inetField: string(), + }) + .primaryKey('id'); + +export const allOptionalTable = table('allOptional') + .from('AllOptional') + .columns({ + id: string(), + optString: string().optional(), + optInt: number().optional(), + optFloat: number().optional(), + optBool: boolean().optional(), + optDateTime: number().optional(), + optJson: json().optional(), + optBigInt: number().optional(), + optDecimal: number().optional(), + optBytes: string().optional(), + optEnum: enumeration().optional(), + optEnumArr: json(), + }) + .primaryKey('id'); + +export const longFieldNamesTable = table('longFieldNames') + .from('long_field_names') + .columns({ + id: string(), + thisIsAVeryLongFieldNameThatMightCauseIssuesInSomeDatabases: + string().from('long_field_1'), + anotherExtremelyLongFieldNameForTestingPurposesAndEdgeCases: + number().from('long_field_2'), + yetAnotherLongFieldNameToEnsureTheGeneratorHandlesThemCorrectly: + boolean().from('long_field_3'), + }) + .primaryKey('id'); + +export const complexJsonTable = table('complexJson') + .from('complex_json') + .columns({ + id: string(), + userPreferences: json(), + formData: json(), + apiResponse: json(), + geoLocation: json(), + metadata: json().optional(), + }) + .primaryKey('id'); + +export const orderTable = table('order') + .from('Order') + .columns({ + id: string(), + orderNumber: string().from('order_number'), + status: enumeration(), + subtotal: number(), + tax: number(), + shipping: number(), + total: number(), + currency: string(), + notes: string().optional(), + shippingAddress: json().from('shipping_address'), + billingAddress: json().from('billing_address'), + createdAt: number(), + updatedAt: number(), + }) + .primaryKey('id'); + +export const orderLineItemTable = table('orderLineItems') + .from('order_line_items') + .columns({ + id: string(), + productId: string().from('product_id'), + productName: string().from('product_name'), + sku: string(), + quantity: number(), + unitPrice: number().from('unit_price'), + totalPrice: number().from('total_price'), + orderId: string().from('order_id'), + }) + .primaryKey('id'); + +export const orderStatusHistoryTable = table('orderStatusHistory') + .from('order_status_history') + .columns({ + id: string(), + fromStatus: enumeration().from('from_status').optional(), + toStatus: enumeration().from('to_status'), + reason: string().optional(), + changedAt: number().from('changed_at'), + changedBy: string().from('changed_by').optional(), + orderId: string().from('order_id'), + }) + .primaryKey('id'); + +export const notificationTable = table('notification') + .from('Notification') + .columns({ + id: string(), + title: string(), + body: string(), + data: json().optional(), + read: boolean(), + channels: json(), + sentVia: json(), + priority: enumeration(), + scheduledFor: number().from('scheduled_for').optional(), + sentAt: number().from('sent_at').optional(), + readAt: number().from('read_at').optional(), + createdAt: number(), + }) + .primaryKey('id'); + +export const reservedWordsTable = table('reservedWords') + .from('reserved_words') + .columns({ + id: string(), + select: string().from('select_field'), + from: string().from('from_field'), + where: string().from('where_field'), + order: string().from('order_field'), + group: string().from('group_field'), + having: string().from('having_field'), + limit: number().from('limit_field'), + offset: number().from('offset_field'), + join: string().from('join_field'), + insert: string().from('insert_field'), + update: string().from('update_field'), + delete: string().from('delete_field'), + create: string().from('create_field'), + drop: string().from('drop_field'), + table: string().from('table_field'), + index: string().from('index_field'), + }) + .primaryKey('id'); + +export const model123WithNumbers456Table = table('model123WithNumbers456') + .from('Model123WithNumbers456') + .columns({ + id: string(), + field1: string(), + field2: number(), + field3: boolean(), + }) + .primaryKey('id'); + +export const minimalModelTable = table('minimalModel') + .from('MinimalModel') + .columns({ + id: string(), + }) + .primaryKey('id'); + +export const autoGeneratedOnlyTable = table('autoGeneratedOnly') + .from('AutoGeneratedOnly') + .columns({ + id: string(), + createdAt: number(), + updatedAt: number(), + }) + .primaryKey('id'); + +export const _projectToTagTable = table('_projectToTag') + .from('_ProjectToTag') + .columns({ + A: string(), + B: string(), + }) + .primaryKey('A', 'B'); + +export const _projectSkillsTable = table('_projectSkills') + .from('_ProjectSkills') + .columns({ + A: string(), + B: string(), + }) + .primaryKey('A', 'B'); + +export const _articleToTagTable = table('_articleToTag') + .from('_ArticleToTag') + .columns({ + A: string(), + B: string(), + }) + .primaryKey('A', 'B'); + +export const _articleToCategoryTable = table('_articleToCategory') + .from('_ArticleToCategory') + .columns({ + A: string(), + B: string(), + }) + .primaryKey('A', 'B'); + +export const _blockedUsersTable = table('_blockedUsers') + .from('_BlockedUsers') + .columns({ + A: string(), + B: string(), + }) + .primaryKey('A', 'B'); + +export const fieldMappingTableRelationships = relationships( + fieldMappingTable, + ({one}) => ({ + organization: one({ + sourceField: ['organizationId'], + destField: ['id'], + destSchema: organizationTable, + }), + }), +); +export const userAccountTableRelationships = relationships( + userAccountTable, + ({one, many}) => ({ + profile: one({ + sourceField: ['id'], + destField: ['userId'], + destSchema: userProfileTable, + }), + sessions: many({ + sourceField: ['id'], + destField: ['userId'], + destSchema: userSessionTable, + }), + }), +); +export const userProfileTableRelationships = relationships( + userProfileTable, + ({one}) => ({ + user: one({ + sourceField: ['userId'], + destField: ['id'], + destSchema: userAccountTable, + }), + }), +); +export const userSessionTableRelationships = relationships( + userSessionTable, + ({one}) => ({ + user: one({ + sourceField: ['userId'], + destField: ['id'], + destSchema: userAccountTable, + }), + }), +); +export const tenantUserTableRelationships = relationships( + tenantUserTable, + ({one}) => ({ + tenant: one({ + sourceField: ['tenantId'], + destField: ['id'], + destSchema: tenantTable, + }), + }), +); +export const tenantApiKeyTableRelationships = relationships( + tenantApiKeyTable, + ({one}) => ({ + tenant: one({ + sourceField: ['tenantId'], + destField: ['id'], + destSchema: tenantTable, + }), + }), +); +export const tenantTableRelationships = relationships( + tenantTable, + ({many}) => ({ + users: many({ + sourceField: ['id'], + destField: ['tenantId'], + destSchema: tenantUserTable, + }), + apiKeys: many({ + sourceField: ['id'], + destField: ['tenantId'], + destSchema: tenantApiKeyTable, + }), + }), +); +export const personTableRelationships = relationships(personTable, ({one}) => ({ + passport: one({ + sourceField: ['id'], + destField: ['personId'], + destSchema: passportTable, + }), + driverLicense: one({ + sourceField: ['id'], + destField: ['personId'], + destSchema: driverLicenseTable, + }), + spouse: one({ + sourceField: ['spouseId'], + destField: ['id'], + destSchema: personTable, + }), + spouseOf: one({ + sourceField: ['id'], + destField: ['spouseId'], + destSchema: personTable, + }), +})); +export const passportTableRelationships = relationships( + passportTable, + ({one}) => ({ + person: one({ + sourceField: ['personId'], + destField: ['id'], + destSchema: personTable, + }), + }), +); +export const driverLicenseTableRelationships = relationships( + driverLicenseTable, + ({one}) => ({ + person: one({ + sourceField: ['personId'], + destField: ['id'], + destSchema: personTable, + }), + }), +); +export const organizationTableRelationships = relationships( + organizationTable, + ({many}) => ({ + departments: many({ + sourceField: ['id'], + destField: ['organizationId'], + destSchema: departmentTable, + }), + employees: many({ + sourceField: ['id'], + destField: ['organizationId'], + destSchema: employeeTable, + }), + projects: many({ + sourceField: ['id'], + destField: ['organizationId'], + destSchema: projectTable, + }), + fieldMappings: many({ + sourceField: ['id'], + destField: ['organizationId'], + destSchema: fieldMappingTable, + }), + }), +); +export const departmentTableRelationships = relationships( + departmentTable, + ({one, many}) => ({ + organization: one({ + sourceField: ['organizationId'], + destField: ['id'], + destSchema: organizationTable, + }), + employees: many({ + sourceField: ['id'], + destField: ['departmentId'], + destSchema: employeeTable, + }), + teams: many({ + sourceField: ['id'], + destField: ['departmentId'], + destSchema: teamTable, + }), + }), +); +export const employeeTableRelationships = relationships( + employeeTable, + ({one, many}) => ({ + organization: one({ + sourceField: ['organizationId'], + destField: ['id'], + destSchema: organizationTable, + }), + department: one({ + sourceField: ['departmentId'], + destField: ['id'], + destSchema: departmentTable, + }), + managedTeam: one({ + sourceField: ['id'], + destField: ['managerId'], + destSchema: teamTable, + }), + teamMemberships: many({ + sourceField: ['id'], + destField: ['employeeId'], + destSchema: teamMemberTable, + }), + assignedTasks: many({ + sourceField: ['id'], + destField: ['assigneeId'], + destSchema: taskTable, + }), + createdTasks: many({ + sourceField: ['id'], + destField: ['creatorId'], + destSchema: taskTable, + }), + }), +); +export const teamTableRelationships = relationships( + teamTable, + ({one, many}) => ({ + department: one({ + sourceField: ['departmentId'], + destField: ['id'], + destSchema: departmentTable, + }), + manager: one({ + sourceField: ['managerId'], + destField: ['id'], + destSchema: employeeTable, + }), + members: many({ + sourceField: ['id'], + destField: ['teamId'], + destSchema: teamMemberTable, + }), + }), +); +export const teamMemberTableRelationships = relationships( + teamMemberTable, + ({one}) => ({ + team: one({ + sourceField: ['teamId'], + destField: ['id'], + destSchema: teamTable, + }), + employee: one({ + sourceField: ['employeeId'], + destField: ['id'], + destSchema: employeeTable, + }), + }), +); +export const projectTableRelationships = relationships( + projectTable, + ({one, many}) => ({ + organization: one({ + sourceField: ['organizationId'], + destField: ['id'], + destSchema: organizationTable, + }), + tags: many( + { + sourceField: ['id'], + destField: ['A'], + destSchema: _projectToTagTable, + }, + { + sourceField: ['B'], + destField: ['id'], + destSchema: tagTable, + }, + ), + tasks: many({ + sourceField: ['id'], + destField: ['projectId'], + destSchema: taskTable, + }), + requiredSkills: many( + { + sourceField: ['id'], + destField: ['A'], + destSchema: _projectSkillsTable, + }, + { + sourceField: ['B'], + destField: ['id'], + destSchema: skillTable, + }, + ), + }), +); +export const tagTableRelationships = relationships(tagTable, ({many}) => ({ + projects: many( + { + sourceField: ['id'], + destField: ['B'], + destSchema: _projectToTagTable, + }, + { + sourceField: ['A'], + destField: ['id'], + destSchema: projectTable, + }, + ), + articles: many( + { + sourceField: ['id'], + destField: ['B'], + destSchema: _articleToTagTable, + }, + { + sourceField: ['A'], + destField: ['id'], + destSchema: articleTable, + }, + ), +})); +export const articleTableRelationships = relationships( + articleTable, + ({many}) => ({ + tags: many( + { + sourceField: ['id'], + destField: ['A'], + destSchema: _articleToTagTable, + }, + { + sourceField: ['B'], + destField: ['id'], + destSchema: tagTable, + }, + ), + categories: many( + { + sourceField: ['id'], + destField: ['A'], + destSchema: _articleToCategoryTable, + }, + { + sourceField: ['B'], + destField: ['id'], + destSchema: categoryTable, + }, + ), + comments: many({ + sourceField: ['id'], + destField: ['articleId'], + destSchema: commentTable, + }), + }), +); +export const categoryTableRelationships = relationships( + categoryTable, + ({one, many}) => ({ + parent: one({ + sourceField: ['parentId'], + destField: ['id'], + destSchema: categoryTable, + }), + children: many({ + sourceField: ['id'], + destField: ['parentId'], + destSchema: categoryTable, + }), + articles: many( + { + sourceField: ['id'], + destField: ['B'], + destSchema: _articleToCategoryTable, + }, + { + sourceField: ['A'], + destField: ['id'], + destSchema: articleTable, + }, + ), + }), +); +export const skillTableRelationships = relationships(skillTable, ({many}) => ({ + workers: many({ sourceField: ['id'], - destField: ['authorId'], - destSchema: postTable, + destField: ['skillId'], + destSchema: workerSkillTable, }), + projects: many( + { + sourceField: ['id'], + destField: ['B'], + destSchema: _projectSkillsTable, + }, + { + sourceField: ['A'], + destField: ['id'], + destSchema: projectTable, + }, + ), })); -export const postTableRelationships = relationships(postTable, ({one}) => ({ - author: one({ - sourceField: ['authorId'], +export const workerTableRelationships = relationships( + workerTable, + ({many}) => ({ + skills: many({ + sourceField: ['id'], + destField: ['workerId'], + destSchema: workerSkillTable, + }), + }), +); +export const workerSkillTableRelationships = relationships( + workerSkillTable, + ({one}) => ({ + worker: one({ + sourceField: ['workerId'], + destField: ['id'], + destSchema: workerTable, + }), + skill: one({ + sourceField: ['skillId'], + destField: ['id'], + destSchema: skillTable, + }), + }), +); +export const commentTableRelationships = relationships( + commentTable, + ({one, many}) => ({ + article: one({ + sourceField: ['articleId'], + destField: ['id'], + destSchema: articleTable, + }), + parent: one({ + sourceField: ['parentId'], + destField: ['id'], + destSchema: commentTable, + }), + replies: many({ + sourceField: ['id'], + destField: ['parentId'], + destSchema: commentTable, + }), + }), +); +export const treeNodeTableRelationships = relationships( + treeNodeTable, + ({one, many}) => ({ + parent: one({ + sourceField: ['parentId'], + destField: ['id'], + destSchema: treeNodeTable, + }), + children: many({ + sourceField: ['id'], + destField: ['parentId'], + destSchema: treeNodeTable, + }), + }), +); +export const socialUserTableRelationships = relationships( + socialUserTable, + ({many}) => ({ + following: many({ + sourceField: ['id'], + destField: ['followerId'], + destSchema: followTable, + }), + followers: many({ + sourceField: ['id'], + destField: ['followingId'], + destSchema: followTable, + }), + blockedUsers: many( + { + sourceField: ['id'], + destField: ['A'], + destSchema: _blockedUsersTable, + }, + { + sourceField: ['B'], + destField: ['id'], + destSchema: socialUserTable, + }, + ), + blockedBy: many( + { + sourceField: ['id'], + destField: ['A'], + destSchema: _blockedUsersTable, + }, + { + sourceField: ['B'], + destField: ['id'], + destSchema: socialUserTable, + }, + ), + }), +); +export const followTableRelationships = relationships(followTable, ({one}) => ({ + follower: one({ + sourceField: ['followerId'], destField: ['id'], - destSchema: userTable, + destSchema: socialUserTable, + }), + following: one({ + sourceField: ['followingId'], + destField: ['id'], + destSchema: socialUserTable, }), })); +export const compositePkParentTableRelationships = relationships( + compositePkParentTable, + ({many}) => ({ + children: many({ + sourceField: ['orgId', 'recordId'], + destField: ['parentOrgId', 'parentRecordId'], + destSchema: compositeFkChildTable, + }), + }), +); +export const compositeFkChildTableRelationships = relationships( + compositeFkChildTable, + ({one}) => ({ + parent: one({ + sourceField: ['parentOrgId', 'parentRecordId'], + destField: ['orgId', 'recordId'], + destSchema: compositePkParentTable, + }), + }), +); +export const taskTableRelationships = relationships(taskTable, ({one}) => ({ + project: one({ + sourceField: ['projectId'], + destField: ['id'], + destSchema: projectTable, + }), + assignee: one({ + sourceField: ['assigneeId'], + destField: ['id'], + destSchema: employeeTable, + }), + creator: one({ + sourceField: ['creatorId'], + destField: ['id'], + destSchema: employeeTable, + }), +})); +export const orderTableRelationships = relationships(orderTable, ({many}) => ({ + lineItems: many({ + sourceField: ['id'], + destField: ['orderId'], + destSchema: orderLineItemTable, + }), + statusHistory: many({ + sourceField: ['id'], + destField: ['orderId'], + destSchema: orderStatusHistoryTable, + }), +})); +export const orderLineItemTableRelationships = relationships( + orderLineItemTable, + ({one}) => ({ + order: one({ + sourceField: ['orderId'], + destField: ['id'], + destSchema: orderTable, + }), + }), +); +export const orderStatusHistoryTableRelationships = relationships( + orderStatusHistoryTable, + ({one}) => ({ + order: one({ + sourceField: ['orderId'], + destField: ['id'], + destSchema: orderTable, + }), + }), +); +export const _projectToTagTableRelationships = relationships( + _projectToTagTable, + ({one}) => ({ + modelA: one({ + sourceField: ['A'], + destField: ['id'], + destSchema: projectTable, + }), + modelB: one({ + sourceField: ['B'], + destField: ['id'], + destSchema: tagTable, + }), + }), +); +export const _projectSkillsTableRelationships = relationships( + _projectSkillsTable, + ({one}) => ({ + modelA: one({ + sourceField: ['A'], + destField: ['id'], + destSchema: projectTable, + }), + modelB: one({ + sourceField: ['B'], + destField: ['id'], + destSchema: skillTable, + }), + }), +); +export const _articleToTagTableRelationships = relationships( + _articleToTagTable, + ({one}) => ({ + modelA: one({ + sourceField: ['A'], + destField: ['id'], + destSchema: articleTable, + }), + modelB: one({ + sourceField: ['B'], + destField: ['id'], + destSchema: tagTable, + }), + }), +); +export const _articleToCategoryTableRelationships = relationships( + _articleToCategoryTable, + ({one}) => ({ + modelA: one({ + sourceField: ['A'], + destField: ['id'], + destSchema: articleTable, + }), + modelB: one({ + sourceField: ['B'], + destField: ['id'], + destSchema: categoryTable, + }), + }), +); +export const _blockedUsersTableRelationships = relationships( + _blockedUsersTable, + ({one}) => ({ + modelA: one({ + sourceField: ['A'], + destField: ['id'], + destSchema: socialUserTable, + }), + modelB: one({ + sourceField: ['B'], + destField: ['id'], + destSchema: socialUserTable, + }), + }), +); /** * The Zero schema object. * This type is auto-generated from your Prisma schema definition. */ export const schema = createSchema({ - tables: [userTable, postTable], - relationships: [userTableRelationships, postTableRelationships], + tables: [ + allScalarTypesTable, + allArrayTypesTable, + defaultValuesTable, + fieldMappingTable, + userAccountTable, + userProfileTable, + userSessionTable, + tenantUserTable, + tenantApiKeyTable, + tenantTable, + uniqueConstraintsTable, + indexedModelTable, + personTable, + passportTable, + driverLicenseTable, + organizationTable, + departmentTable, + employeeTable, + teamTable, + teamMemberTable, + projectTable, + tagTable, + articleTable, + categoryTable, + skillTable, + workerTable, + workerSkillTable, + commentTable, + treeNodeTable, + socialUserTable, + followTable, + compositePkParentTable, + compositeFkChildTable, + taskTable, + nativeTypesTable, + allOptionalTable, + longFieldNamesTable, + complexJsonTable, + orderTable, + orderLineItemTable, + orderStatusHistoryTable, + notificationTable, + reservedWordsTable, + model123WithNumbers456Table, + minimalModelTable, + autoGeneratedOnlyTable, + _projectToTagTable, + _projectSkillsTable, + _articleToTagTable, + _articleToCategoryTable, + _blockedUsersTable, + ], + relationships: [ + fieldMappingTableRelationships, + userAccountTableRelationships, + userProfileTableRelationships, + userSessionTableRelationships, + tenantUserTableRelationships, + tenantApiKeyTableRelationships, + tenantTableRelationships, + personTableRelationships, + passportTableRelationships, + driverLicenseTableRelationships, + organizationTableRelationships, + departmentTableRelationships, + employeeTableRelationships, + teamTableRelationships, + teamMemberTableRelationships, + projectTableRelationships, + tagTableRelationships, + articleTableRelationships, + categoryTableRelationships, + skillTableRelationships, + workerTableRelationships, + workerSkillTableRelationships, + commentTableRelationships, + treeNodeTableRelationships, + socialUserTableRelationships, + followTableRelationships, + compositePkParentTableRelationships, + compositeFkChildTableRelationships, + taskTableRelationships, + orderTableRelationships, + orderLineItemTableRelationships, + orderStatusHistoryTableRelationships, + _projectToTagTableRelationships, + _projectSkillsTableRelationships, + _articleToTagTableRelationships, + _articleToCategoryTableRelationships, + _blockedUsersTableRelationships, + ], }); /** diff --git a/integration/schema.prisma b/integration/schema.prisma index c28e274..37b6067 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -3,24 +3,989 @@ datasource db { } generator zero { - provider = "prisma-zero" - output = "./generated/zero" - prettier = true + provider = "prisma-zero" + output = "./generated/zero" + prettier = true + camelCase = true + excludeTables = ["IgnoredModel", "InternalAuditLog"] } -model User { +// ============================================================================ +// ENUMS - All enum variations +// ============================================================================ + +/// Basic enum +enum Role { + USER + ADMIN + MODERATOR + SUPER_ADMIN +} + +/// Enum with database name mapping +enum Status { + PENDING @map("pending") + ACTIVE @map("active") + INACTIVE @map("inactive") + ARCHIVED @map("archived") + DELETED @map("deleted") +} + +/// Enum for order status +enum OrderStatus { + DRAFT + SUBMITTED + PROCESSING + SHIPPED + DELIVERED + CANCELLED + REFUNDED +} + +/// Enum for priority levels +enum Priority { + LOW + MEDIUM + HIGH + CRITICAL +} + +/// Enum for notification types +enum NotificationType { + EMAIL + SMS + PUSH + IN_APP + WEBHOOK +} + +// ============================================================================ +// MODELS WITH ALL SCALAR TYPES +// ============================================================================ + +/// Model demonstrating ALL scalar types available in Prisma +model AllScalarTypes { + id String @id @default(uuid()) + + // Basic types + stringField String + intField Int + floatField Float + booleanField Boolean + dateTimeField DateTime + jsonField Json + bigIntField BigInt + decimalField Decimal + bytesField Bytes + + // Optional versions of all types + optionalString String? + optionalInt Int? + optionalFloat Float? + optionalBoolean Boolean? + optionalDateTime DateTime? + optionalJson Json? + optionalBigInt BigInt? + optionalDecimal Decimal? + optionalBytes Bytes? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +/// Model demonstrating ALL array types +model AllArrayTypes { + id String @id @default(uuid()) + + // Array types (PostgreSQL supports these natively) + stringArray String[] + intArray Int[] + floatArray Float[] + booleanArray Boolean[] + dateTimeArray DateTime[] + jsonArray Json[] + bigIntArray BigInt[] + decimalArray Decimal[] + bytesArray Bytes[] + + // Enum arrays + roles Role[] + statuses Status[] + notificationTypes NotificationType[] + + createdAt DateTime @default(now()) +} + +// ============================================================================ +// MODELS WITH VARIOUS DEFAULT VALUES +// ============================================================================ + +/// Model demonstrating all @default variations +model DefaultValues { + // UUID default (most common) + id String @id @default(uuid()) + + // Auto-increment (for Int IDs) + sequenceNum Int @unique @default(autoincrement()) + + // CUID default + cuidField String @unique @default(cuid()) + + // Static defaults + defaultString String @default("default_value") + defaultInt Int @default(0) + defaultFloat Float @default(0.0) + defaultBool Boolean @default(false) + defaultTrue Boolean @default(true) + + // Database-generated defaults + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Enum with default + status Status @default(PENDING) + role Role @default(USER) + + // JSON with default + metadata Json @default("{}") + settings Json @default("{\"theme\": \"dark\", \"notifications\": true}") + + // Array with default + tags String[] @default([]) +} + +// ============================================================================ +// FIELD MAPPING WITH @map +// ============================================================================ + +/// Model demonstrating field name mapping +model FieldMapping { + id String @id @default(uuid()) + + // Simple snake_case to camelCase mapping + firstName String @map("first_name") + lastName String @map("last_name") + emailAddr String @map("email_address") @unique + phoneNumber String @map("phone_number") + + // Mapping with different naming conventions + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + // Boolean with mapping + isActive Boolean @default(true) @map("is_active") + isVerified Boolean @default(false) @map("is_verified") + + // Foreign key with mapping + organizationId String @map("organization_id") + organization Organization @relation(fields: [organizationId], references: [id]) +} + +// ============================================================================ +// TABLE MAPPING WITH @@map +// ============================================================================ + +/// Model with table name mapping +model UserAccount { + id String @id @default(uuid()) + username String @unique + email String @unique + createdAt DateTime @default(now()) + + profile UserProfile? + sessions UserSession[] + + @@map("user_accounts") +} + +/// Another model with table mapping +model UserProfile { + id String @id @default(uuid()) + bio String? + avatarUrl String? @map("avatar_url") + website String? + + userId String @unique @map("user_id") + user UserAccount @relation(fields: [userId], references: [id]) + + @@map("user_profiles") +} + +/// Session model with mapping +model UserSession { + id String @id @default(uuid()) + token String @unique + userAgent String? @map("user_agent") + ipAddress String? @map("ip_address") + lastActiveAt DateTime @map("last_active_at") + expiresAt DateTime @map("expires_at") + + userId String @map("user_id") + user UserAccount @relation(fields: [userId], references: [id]) + + @@map("user_sessions") +} + +// ============================================================================ +// COMPOSITE PRIMARY KEYS (@@id) +// ============================================================================ + +/// Model with composite primary key +model TenantUser { + tenantId String + userId String + role Role @default(USER) + joinedAt DateTime @default(now()) + + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@id([tenantId, userId]) +} + +/// Another composite key model - for API keys scoped to tenant +model TenantApiKey { + tenantId String + keyId String + name String + hashedKey String @map("hashed_key") + permissions String[] + expiresAt DateTime? @map("expires_at") + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@id([tenantId, keyId]) + @@map("tenant_api_keys") +} + +/// Tenant model +model Tenant { + id String @id @default(uuid()) + name String + slug String @unique + createdAt DateTime @default(now()) + + users TenantUser[] + apiKeys TenantApiKey[] +} + +// ============================================================================ +// COMPOSITE UNIQUE CONSTRAINTS (@@unique) +// ============================================================================ + +/// Model with various unique constraints +model UniqueConstraints { + id String @id @default(uuid()) + + // Single field unique + email String @unique + username String @unique + + // Fields for composite unique + orgId String @map("org_id") + employeeId String @map("employee_id") + + // Additional fields + firstName String @map("first_name") + lastName String @map("last_name") + + @@unique([orgId, employeeId]) + @@unique([orgId, email]) + @@map("unique_constraints") +} + +// ============================================================================ +// INDEXES (@@index) +// ============================================================================ + +/// Model demonstrating various index types +model IndexedModel { + id String @id @default(uuid()) + + // Frequently queried fields + email String + status Status + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Foreign keys + categoryId String @map("category_id") + authorId String @map("author_id") + + // Text search fields + title String + content String? + + // Single column indexes + @@index([email]) + @@index([status]) + @@index([createdAt]) + + // Composite indexes + @@index([categoryId, status]) + @@index([authorId, createdAt]) + @@index([status, createdAt]) + + @@map("indexed_models") +} + +// ============================================================================ +// ONE-TO-ONE RELATIONSHIPS +// ============================================================================ + +/// Person model for 1:1 relationship +model Person { id String @id @default(uuid()) email String @unique name String createdAt DateTime @default(now()) - posts Post[] + + // One-to-one: Person has one Passport + passport Passport? + + // One-to-one: Person has one DriverLicense + driverLicense DriverLicense? + + // One-to-one with same table (spouse) + spouseId String? @unique + spouse Person? @relation("Spouse", fields: [spouseId], references: [id]) + spouseOf Person? @relation("Spouse") +} + +/// Passport - required one-to-one +model Passport { + id String @id @default(uuid()) + passportNumber String @unique @map("passport_number") + country String + expiryDate DateTime @map("expiry_date") + + personId String @unique @map("person_id") + person Person @relation(fields: [personId], references: [id]) +} + +/// DriverLicense - optional one-to-one +model DriverLicense { + id String @id @default(uuid()) + licenseNumber String @unique @map("license_number") + state String + expiryDate DateTime @map("expiry_date") + class String @default("C") + + personId String @unique @map("person_id") + person Person @relation(fields: [personId], references: [id]) + + @@map("driver_licenses") +} + +// ============================================================================ +// ONE-TO-MANY RELATIONSHIPS +// ============================================================================ + +/// Organization for 1:N relationships +model Organization { + id String @id @default(uuid()) + name String + slug String @unique + description String? + website String? + createdAt DateTime @default(now()) + + // One org has many departments + departments Department[] + + // One org has many employees + employees Employee[] + + // One org has many projects + projects Project[] + + // Field mappings from other models + fieldMappings FieldMapping[] +} + +/// Department belongs to Organization +model Department { + id String @id @default(uuid()) + name String + code String + description String? + budget Decimal? + + organizationId String @map("organization_id") + organization Organization @relation(fields: [organizationId], references: [id]) + + // One department has many employees + employees Employee[] + + // One department has many teams + teams Team[] + + @@unique([organizationId, code]) } -model Post { +/// Employee belongs to Organization and Department +model Employee { + id String @id @default(uuid()) + employeeNo String @unique @map("employee_no") + firstName String @map("first_name") + lastName String @map("last_name") + email String @unique + title String? + salary Decimal? + hireDate DateTime @map("hire_date") + + organizationId String @map("organization_id") + organization Organization @relation(fields: [organizationId], references: [id]) + + departmentId String? @map("department_id") + department Department? @relation(fields: [departmentId], references: [id]) + + // Employee can manage a team + managedTeam Team? @relation("TeamManager") + + // Employee belongs to teams + teamMemberships TeamMember[] + + // Employee has assigned tasks + assignedTasks Task[] @relation("TaskAssignee") + + // Employee created tasks + createdTasks Task[] @relation("TaskCreator") +} + +/// Team belongs to Department +model Team { + id String @id @default(uuid()) + name String + description String? + createdAt DateTime @default(now()) + + departmentId String @map("department_id") + department Department @relation(fields: [departmentId], references: [id]) + + // Team has one manager (Employee) + managerId String? @unique @map("manager_id") + manager Employee? @relation("TeamManager", fields: [managerId], references: [id]) + + // Team has many members through TeamMember + members TeamMember[] +} + +/// Junction table for Team-Employee (explicit many-to-many with extra fields) +model TeamMember { + id String @id @default(uuid()) + role String @default("member") + joinedAt DateTime @default(now()) @map("joined_at") + + teamId String @map("team_id") + team Team @relation(fields: [teamId], references: [id]) + + employeeId String @map("employee_id") + employee Employee @relation(fields: [employeeId], references: [id]) + + @@unique([teamId, employeeId]) + @@map("team_members") +} + +// ============================================================================ +// MANY-TO-MANY RELATIONSHIPS (IMPLICIT) +// ============================================================================ + +/// Project with implicit M:N to Tag +model Project { + id String @id @default(uuid()) + name String + description String? + status Status @default(PENDING) + priority Priority @default(MEDIUM) + startDate DateTime? @map("start_date") + endDate DateTime? @map("end_date") + budget Decimal? + createdAt DateTime @default(now()) + + organizationId String @map("organization_id") + organization Organization @relation(fields: [organizationId], references: [id]) + + // Implicit many-to-many with Tag + tags Tag[] + + // Project has many tasks + tasks Task[] + + // Implicit many-to-many with Skill (skills required for project) + requiredSkills Skill[] @relation("ProjectSkills") +} + +/// Tag with implicit M:N to Project +model Tag { id String @id @default(uuid()) + name String @unique + color String @default("#808080") + createdAt DateTime @default(now()) + + // Implicit many-to-many with Project + projects Project[] + + // Implicit many-to-many with Article + articles Article[] +} + +/// Article with implicit M:N to Tag and Category +model Article { + id String @id @default(uuid()) + title String + slug String @unique + content String + excerpt String? + publishedAt DateTime? @map("published_at") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Many-to-many with Tag (implicit) + tags Tag[] + + // Many-to-many with Category (implicit) + categories Category[] + + // Article has many comments + comments Comment[] +} + +/// Category with implicit M:N to Article +model Category { + id String @id @default(uuid()) + name String + slug String @unique + description String? + + // Self-referential: parent category + parentId String? @map("parent_id") + parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id]) + children Category[] @relation("CategoryHierarchy") + + // Many-to-many with Article (implicit) + articles Article[] +} + +// ============================================================================ +// MANY-TO-MANY RELATIONSHIPS (EXPLICIT WITH EXTRA DATA) +// ============================================================================ + +/// Skill model +model Skill { + id String @id @default(uuid()) + name String @unique + description String? + category String? + + // Explicit M:N with Worker through WorkerSkill + workers WorkerSkill[] + + // Implicit M:N with Project + projects Project[] @relation("ProjectSkills") +} + +/// Worker model +model Worker { + id String @id @default(uuid()) + name String + email String @unique + hourlyRate Decimal @map("hourly_rate") + + // Explicit M:N with Skill through WorkerSkill + skills WorkerSkill[] +} + +/// Junction table with extra data (proficiency level, years of experience) +model WorkerSkill { + id String @id @default(uuid()) + proficiency Int @default(1) // 1-5 scale + yearsExperience Int @default(0) @map("years_experience") + certified Boolean @default(false) + certifiedAt DateTime? @map("certified_at") + + workerId String @map("worker_id") + worker Worker @relation(fields: [workerId], references: [id]) + + skillId String @map("skill_id") + skill Skill @relation(fields: [skillId], references: [id]) + + @@unique([workerId, skillId]) + @@map("worker_skills") +} + +// ============================================================================ +// SELF-REFERENTIAL RELATIONSHIPS +// ============================================================================ + +/// Comment with self-referential replies +model Comment { + id String @id @default(uuid()) + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Comment belongs to Article + articleId String @map("article_id") + article Article @relation(fields: [articleId], references: [id]) + + // Self-referential: parent comment (for replies) + parentId String? @map("parent_id") + parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) + replies Comment[] @relation("CommentReplies") +} + +/// Tree node with self-referential parent/children +model TreeNode { + id String @id @default(uuid()) + name String + level Int @default(0) + path String // Materialized path like "/1/2/3" + createdAt DateTime @default(now()) + + parentId String? @map("parent_id") + parent TreeNode? @relation("TreeHierarchy", fields: [parentId], references: [id]) + children TreeNode[] @relation("TreeHierarchy") + + @@map("tree_nodes") +} + +/// Social graph: User follows User +model SocialUser { + id String @id @default(uuid()) + username String @unique + name String + bio String? + createdAt DateTime @default(now()) + + // Self-referential M:N through Follow junction + following Follow[] @relation("Follower") + followers Follow[] @relation("Following") + + // Self-referential: blocked users (implicit M:N) + blockedUsers SocialUser[] @relation("BlockedUsers") + blockedBy SocialUser[] @relation("BlockedUsers") + + @@map("social_users") +} + +/// Explicit junction for follow relationship with timestamp +model Follow { + id String @id @default(uuid()) + followedAt DateTime @default(now()) @map("followed_at") + + followerId String @map("follower_id") + follower SocialUser @relation("Follower", fields: [followerId], references: [id]) + + followingId String @map("following_id") + following SocialUser @relation("Following", fields: [followingId], references: [id]) + + @@unique([followerId, followingId]) +} + +// ============================================================================ +// COMPOSITE FOREIGN KEYS +// ============================================================================ + +/// Model with composite primary key for FK reference +model CompositePKParent { + orgId String + recordId String + name String + createdAt DateTime @default(now()) + + // Has many children + children CompositeFKChild[] + + @@id([orgId, recordId]) + @@map("composite_pk_parents") +} + +/// Model referencing composite FK +model CompositeFKChild { + id String @id @default(uuid()) + name String + value Int + createdAt DateTime @default(now()) + + // Composite foreign key + parentOrgId String @map("parent_org_id") + parentRecordId String @map("parent_record_id") + + parent CompositePKParent @relation(fields: [parentOrgId, parentRecordId], references: [orgId, recordId]) + + @@index([parentOrgId, parentRecordId]) + @@map("composite_fk_children") +} + +// ============================================================================ +// TASKS WITH MULTIPLE RELATIONSHIPS TO SAME MODEL +// ============================================================================ + +/// Task with multiple relations to Employee +model Task { + id String @id @default(uuid()) + title String + description String? + status Status @default(PENDING) + priority Priority @default(MEDIUM) + dueDate DateTime? @map("due_date") + completedAt DateTime? @map("completed_at") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Task belongs to Project + projectId String @map("project_id") + project Project @relation(fields: [projectId], references: [id]) + + // Task has assignee (Employee) - can be null + assigneeId String? @map("assignee_id") + assignee Employee? @relation("TaskAssignee", fields: [assigneeId], references: [id]) + + // Task has creator (Employee) + creatorId String @map("creator_id") + creator Employee @relation("TaskCreator", fields: [creatorId], references: [id]) +} + +// ============================================================================ +// NATIVE DATABASE TYPES (@db.*) +// ============================================================================ + +/// Model using PostgreSQL native types +model NativeTypes { + id String @id @default(uuid()) @db.Uuid + + // String variations + varcharField String @db.VarChar(255) + charField String @db.Char(10) + textField String @db.Text + + // Numeric variations + smallIntField Int @db.SmallInt + integerField Int @db.Integer + bigIntField BigInt @db.BigInt + realField Float @db.Real + doublePrecision Float @db.DoublePrecision + decimalField Decimal @db.Decimal(10, 2) + moneyField Decimal @db.Money + + // Date/Time variations + timestampField DateTime @db.Timestamp(6) + timestampTz DateTime @db.Timestamptz(6) + dateField DateTime @db.Date + timeField DateTime @db.Time(6) + timeTzField DateTime @db.Timetz(6) + + // Boolean + boolField Boolean @db.Boolean + + // Binary + byteaField Bytes @db.ByteA + + // JSON variations + jsonField Json @db.Json + jsonbField Json @db.JsonB + + // Special types + uuidField String @db.Uuid + xmlField String @db.Xml + inetField String @db.Inet + + @@map("native_types") +} + +// ============================================================================ +// MODELS TO BE EXCLUDED (via excludeTables config) +// ============================================================================ + +/// This model should be excluded from Zero schema +model IgnoredModel { + id String @id @default(uuid()) + name String + secretKey String @map("secret_key") + createdAt DateTime @default(now()) +} + +/// Internal audit log - excluded +model InternalAuditLog { + id String @id @default(uuid()) + action String + tableName String @map("table_name") + recordId String @map("record_id") + oldData Json? @map("old_data") + newData Json? @map("new_data") + userId String? @map("user_id") + ipAddress String? @map("ip_address") + createdAt DateTime @default(now()) + + @@index([tableName, recordId]) + @@index([userId]) + @@map("internal_audit_logs") +} + +// ============================================================================ +// EDGE CASES AND SPECIAL SCENARIOS +// ============================================================================ + +/// Model with all optional fields (except ID) +model AllOptional { + id String @id @default(uuid()) + + optString String? + optInt Int? + optFloat Float? + optBool Boolean? + optDateTime DateTime? + optJson Json? + optBigInt BigInt? + optDecimal Decimal? + optBytes Bytes? + optEnum Status? + optEnumArr Role[] +} + +/// Model with very long field names +model LongFieldNames { + id String @id @default(uuid()) + + thisIsAVeryLongFieldNameThatMightCauseIssuesInSomeDatabases String @map("long_field_1") + anotherExtremelyLongFieldNameForTestingPurposesAndEdgeCases Int @map("long_field_2") + yetAnotherLongFieldNameToEnsureTheGeneratorHandlesThemCorrectly Boolean @map("long_field_3") + + @@map("long_field_names") +} + +/// Model with JSON containing complex nested structure hint +model ComplexJson { + id String @id @default(uuid()) + + // These would benefit from typed JSON in the future + userPreferences Json // { theme: string, language: string, notifications: { email: boolean, push: boolean } } + formData Json // Dynamic form submissions + apiResponse Json // Cached API responses + geoLocation Json // { lat: number, lng: number, accuracy: number } + metadata Json? // Optional complex metadata + + @@map("complex_json") +} + +/// Order model for e-commerce scenario +model Order { + id String @id @default(uuid()) + orderNumber String @unique @map("order_number") + status OrderStatus @default(DRAFT) + subtotal Decimal @db.Decimal(10, 2) + tax Decimal @db.Decimal(10, 2) + shipping Decimal @db.Decimal(10, 2) + total Decimal @db.Decimal(10, 2) + currency String @default("USD") @db.Char(3) + notes String? + + // Shipping address as JSON (denormalized for simplicity) + shippingAddress Json @map("shipping_address") + billingAddress Json @map("billing_address") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Order has many line items + lineItems OrderLineItem[] + + // Order has many status history entries + statusHistory OrderStatusHistory[] +} + +/// Order line item +model OrderLineItem { + id String @id @default(uuid()) + productId String @map("product_id") + productName String @map("product_name") + sku String + quantity Int + unitPrice Decimal @db.Decimal(10, 2) @map("unit_price") + totalPrice Decimal @db.Decimal(10, 2) @map("total_price") + + orderId String @map("order_id") + order Order @relation(fields: [orderId], references: [id]) + + @@map("order_line_items") +} + +/// Order status history for audit trail +model OrderStatusHistory { + id String @id @default(uuid()) + fromStatus OrderStatus? @map("from_status") + toStatus OrderStatus @map("to_status") + reason String? + changedAt DateTime @default(now()) @map("changed_at") + changedBy String? @map("changed_by") + + orderId String @map("order_id") + order Order @relation(fields: [orderId], references: [id]) + + @@index([orderId, changedAt]) + @@map("order_status_history") +} + +/// Notification model with enum arrays +model Notification { + id String @id @default(uuid()) title String - content String? - published Boolean @default(false) - authorId String - author User @relation(fields: [authorId], references: [id]) + body String + data Json? + read Boolean @default(false) + channels NotificationType[] // Which channels to send to + sentVia NotificationType[] // Which channels it was actually sent through + priority Priority @default(MEDIUM) + + scheduledFor DateTime? @map("scheduled_for") + sentAt DateTime? @map("sent_at") + readAt DateTime? @map("read_at") + createdAt DateTime @default(now()) + + @@index([read, createdAt]) +} + +/// Model to test reserved words as field names +model ReservedWords { + id String @id @default(uuid()) + select String @map("select_field") + from String @map("from_field") + where String @map("where_field") + order String @map("order_field") + group String @map("group_field") + having String @map("having_field") + limit Int @map("limit_field") + offset Int @map("offset_field") + join String @map("join_field") + insert String @map("insert_field") + update String @map("update_field") + delete String @map("delete_field") + create String @map("create_field") + drop String @map("drop_field") + table String @map("table_field") + index String @map("index_field") + + @@map("reserved_words") +} + +/// Model with numbers in name +model Model123WithNumbers456 { + id String @id @default(uuid()) + field1 String + field2 Int + field3 Boolean +} + +/// Model testing minimum viable setup +model MinimalModel { + id String @id +} + +/// Model with only auto-generated fields +model AutoGeneratedOnly { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/src/generators/code-generator.ts b/src/generators/code-generator.ts index ab4ede5..6152121 100644 --- a/src/generators/code-generator.ts +++ b/src/generators/code-generator.ts @@ -18,8 +18,10 @@ function generateImports(schema: TransformedSchema): string { // Check which type functions are used in the schema schema.models.forEach(model => { Object.values(model.columns).forEach(mapping => { - // Extract the base type (e.g., "string()" -> "string", "enumeration(...)" -> "enumeration") - const baseType = mapping.type.split('(')[0]; + // Extract the base type (e.g., "string()" -> "string", "enumeration()" -> "enumeration", "json()" -> "json") + // Handle both generic types like "enumeration()" and simple types like "string()" + const match = mapping.type.match(/^([a-z]+)/); + const baseType = match?.[1]; if (baseType) { usedImports.add(baseType); } diff --git a/src/mappers/schema-mapper.ts b/src/mappers/schema-mapper.ts index e8cd837..d83308d 100644 --- a/src/mappers/schema-mapper.ts +++ b/src/mappers/schema-mapper.ts @@ -309,7 +309,14 @@ export function transformSchema( if (backReference?.isList) { // Only create the join table once for each relationship - if (model.name.localeCompare(targetModel.name) < 0) { + // For self-referential relations (model === targetModel), use field name comparison + // For different models, use model name comparison + const isSelfReferential = model.name === targetModel.name; + const shouldCreate = isSelfReferential + ? field.name.localeCompare(backReference.name) < 0 + : model.name.localeCompare(targetModel.name) < 0; + + if (shouldCreate) { return createImplicitManyToManyModel( model, targetModel, diff --git a/tests/generator.test.ts b/tests/generator.test.ts index 4b1aa28..8bf38ca 100644 --- a/tests/generator.test.ts +++ b/tests/generator.test.ts @@ -174,7 +174,7 @@ describe('Generator', () => { createBuilder, createCRUDBuilder, createSchema, - enumeration, + enumeration, string, table, } from "@rocicorp/zero"; @@ -708,9 +708,7 @@ describe('Generator', () => { createBuilder, createCRUDBuilder, createSchema, - json, - json, - json, + json, string, table, } from "@rocicorp/zero"; @@ -798,7 +796,7 @@ describe('Generator', () => { createBuilder, createCRUDBuilder, createSchema, - json, + json, string, table, } from "@rocicorp/zero"; From 6944e5ad8b89833b1a5d569840ee59ffe9774b25 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Dec 2025 06:22:43 +0000 Subject: [PATCH 2/8] refactor: streamlined integration test schema with clear test annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduced schema from 990+ lines to 316 lines while maintaining full coverage - Each model now has clear /// TEST: comments explaining what it tests - Organized into logical sections: Scalars, Mappings, PKs, Relationships, Edge Cases Bug fix: - Fixed self-referential M:N relationships not finding correct backReference (find() was returning same field instead of the paired relation field) Coverage maintained: - All scalar types (String, Int, Float, Boolean, DateTime, Json, BigInt, Decimal, Bytes) - Array types → json() - Enums with @map, enum arrays - Field @map and table @@map - Composite @@id primary keys - All relationship types: 1:1, 1:N, implicit M:N, explicit M:N, self-referential - Multiple relations to same model (creator/assignee pattern) - Native @db.* types - excludeTables config - Edge cases (minimal model, reserved words) --- integration/generated/zero/schema.ts | 1315 +++++--------------------- integration/schema.prisma | 1061 ++++----------------- src/mappers/schema-mapper.ts | 10 +- 3 files changed, 421 insertions(+), 1965 deletions(-) diff --git a/integration/generated/zero/schema.ts b/integration/generated/zero/schema.ts index e25a622..412400b 100644 --- a/integration/generated/zero/schema.ts +++ b/integration/generated/zero/schema.ts @@ -14,88 +14,47 @@ import { table, } from '@rocicorp/zero'; -export type Role = 'USER' | 'ADMIN' | 'MODERATOR' | 'SUPER_ADMIN'; +export type Role = 'USER' | 'ADMIN'; -export type Status = 'pending' | 'active' | 'inactive' | 'archived' | 'deleted'; +export type Status = 'active' | 'inactive'; -export type OrderStatus = - | 'DRAFT' - | 'SUBMITTED' - | 'PROCESSING' - | 'SHIPPED' - | 'DELIVERED' - | 'CANCELLED' - | 'REFUNDED'; - -export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; - -export type NotificationType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP' | 'WEBHOOK'; - -export const allScalarTypesTable = table('allScalarTypes') - .from('AllScalarTypes') +export const scalarTypesTable = table('scalarTypes') + .from('ScalarTypes') .columns({ id: string(), - stringField: string(), - intField: number(), - floatField: number(), - booleanField: boolean(), - dateTimeField: number(), - jsonField: json(), - bigIntField: number(), - decimalField: number(), - bytesField: string(), - optionalString: string().optional(), - optionalInt: number().optional(), - optionalFloat: number().optional(), - optionalBoolean: boolean().optional(), - optionalDateTime: number().optional(), - optionalJson: json().optional(), - optionalBigInt: number().optional(), - optionalDecimal: number().optional(), - optionalBytes: string().optional(), - createdAt: number(), - updatedAt: number(), + str: string(), + int: number(), + float: number(), + bool: boolean(), + dateTime: number(), + json: json(), + bigInt: number(), + decimal: number(), + bytes: string(), }) .primaryKey('id'); -export const allArrayTypesTable = table('allArrayTypes') - .from('AllArrayTypes') +export const optionalTypesTable = table('optionalTypes') + .from('OptionalTypes') .columns({ id: string(), - stringArray: json(), - intArray: json(), - floatArray: json(), - booleanArray: json(), - dateTimeArray: json(), - jsonArray: json(), - bigIntArray: json(), - decimalArray: json(), - bytesArray: json(), - roles: json(), - statuses: json(), - notificationTypes: json(), - createdAt: number(), + str: string().optional(), + int: number().optional(), + dateTime: number().optional(), + json: json().optional(), + enum: enumeration().optional(), }) .primaryKey('id'); -export const defaultValuesTable = table('defaultValues') - .from('DefaultValues') +export const arrayTypesTable = table('arrayTypes') + .from('ArrayTypes') .columns({ id: string(), - sequenceNum: number(), - cuidField: string(), - defaultString: string(), - defaultInt: number(), - defaultFloat: number(), - defaultBool: boolean(), - defaultTrue: boolean(), - createdAt: number(), - updatedAt: number(), - status: enumeration(), - role: enumeration(), - metadata: json(), - settings: json(), - tags: json(), + strings: json(), + ints: json(), + bools: json(), + enums: json(), + jsonArray: json(), }) .primaryKey('id'); @@ -105,222 +64,74 @@ export const fieldMappingTable = table('fieldMapping') id: string(), firstName: string().from('first_name'), lastName: string().from('last_name'), - emailAddr: string().from('email_address'), - phoneNumber: string().from('phone_number'), - createdAt: number().from('created_at'), - updatedAt: number().from('updated_at'), - deletedAt: number().from('deleted_at').optional(), - isActive: boolean().from('is_active'), - isVerified: boolean().from('is_verified'), - organizationId: string().from('organization_id'), - }) - .primaryKey('id'); - -export const userAccountTable = table('userAccounts') - .from('user_accounts') - .columns({ - id: string(), - username: string(), - email: string(), - createdAt: number(), }) .primaryKey('id'); -export const userProfileTable = table('userProfiles') - .from('user_profiles') - .columns({ - id: string(), - bio: string().optional(), - avatarUrl: string().from('avatar_url').optional(), - website: string().optional(), - userId: string().from('user_id'), - }) - .primaryKey('id'); - -export const userSessionTable = table('userSessions') - .from('user_sessions') - .columns({ - id: string(), - token: string(), - userAgent: string().from('user_agent').optional(), - ipAddress: string().from('ip_address').optional(), - lastActiveAt: number().from('last_active_at'), - expiresAt: number().from('expires_at'), - userId: string().from('user_id'), - }) - .primaryKey('id'); - -export const tenantUserTable = table('tenantUser') - .from('TenantUser') - .columns({ - tenantId: string(), - userId: string(), - role: enumeration(), - joinedAt: number(), - }) - .primaryKey('tenantId', 'userId'); - -export const tenantApiKeyTable = table('tenantApiKeys') - .from('tenant_api_keys') - .columns({ - tenantId: string(), - keyId: string(), - name: string(), - hashedKey: string().from('hashed_key'), - permissions: json(), - expiresAt: number().from('expires_at').optional(), - createdAt: number().from('created_at'), - }) - .primaryKey('tenantId', 'keyId'); - -export const tenantTable = table('tenant') - .from('Tenant') +export const tableMappingTable = table('tableMappings') + .from('table_mappings') .columns({ id: string(), name: string(), - slug: string(), - createdAt: number(), - }) - .primaryKey('id'); - -export const uniqueConstraintsTable = table('uniqueConstraints') - .from('unique_constraints') - .columns({ - id: string(), - email: string(), - username: string(), - orgId: string().from('org_id'), - employeeId: string().from('employee_id'), - firstName: string().from('first_name'), - lastName: string().from('last_name'), }) .primaryKey('id'); -export const indexedModelTable = table('indexedModels') - .from('indexed_models') +export const combinedMappingTable = table('combinedMappings') + .from('combined_mappings') .columns({ id: string(), - email: string(), - status: enumeration(), createdAt: number().from('created_at'), - updatedAt: number().from('updated_at'), - categoryId: string().from('category_id'), - authorId: string().from('author_id'), - title: string(), - content: string().optional(), - }) - .primaryKey('id'); - -export const personTable = table('person') - .from('Person') - .columns({ - id: string(), - email: string(), - name: string(), - createdAt: number(), - spouseId: string().optional(), - }) - .primaryKey('id'); - -export const passportTable = table('passport') - .from('Passport') - .columns({ - id: string(), - passportNumber: string().from('passport_number'), - country: string(), - expiryDate: number().from('expiry_date'), - personId: string().from('person_id'), }) .primaryKey('id'); -export const driverLicenseTable = table('driverLicenses') - .from('driver_licenses') +export const compositePkTable = table('compositePk') + .from('CompositePK') .columns({ - id: string(), - licenseNumber: string().from('license_number'), - state: string(), - expiryDate: number().from('expiry_date'), - class: string(), - personId: string().from('person_id'), - }) - .primaryKey('id'); - -export const organizationTable = table('organization') - .from('Organization') - .columns({ - id: string(), - name: string(), - slug: string(), - description: string().optional(), - website: string().optional(), - createdAt: number(), + tenantId: string(), + recordId: string(), + data: string(), }) - .primaryKey('id'); + .primaryKey('tenantId', 'recordId'); -export const departmentTable = table('department') - .from('Department') +export const userTable = table('user') + .from('User') .columns({ id: string(), - name: string(), - code: string(), - description: string().optional(), - budget: number().optional(), - organizationId: string().from('organization_id'), + email: string(), }) .primaryKey('id'); -export const employeeTable = table('employee') - .from('Employee') +export const profileTable = table('profile') + .from('Profile') .columns({ id: string(), - employeeNo: string().from('employee_no'), - firstName: string().from('first_name'), - lastName: string().from('last_name'), - email: string(), - title: string().optional(), - salary: number().optional(), - hireDate: number().from('hire_date'), - organizationId: string().from('organization_id'), - departmentId: string().from('department_id').optional(), + bio: string().optional(), + userId: string(), }) .primaryKey('id'); -export const teamTable = table('team') - .from('Team') +export const postTable = table('post') + .from('Post') .columns({ id: string(), - name: string(), - description: string().optional(), - createdAt: number(), - departmentId: string().from('department_id'), - managerId: string().from('manager_id').optional(), + title: string(), + authorId: string(), }) .primaryKey('id'); -export const teamMemberTable = table('teamMembers') - .from('team_members') +export const commentTable = table('comment') + .from('Comment') .columns({ id: string(), - role: string(), - joinedAt: number().from('joined_at'), - teamId: string().from('team_id'), - employeeId: string().from('employee_id'), + text: string(), + postId: string(), }) .primaryKey('id'); -export const projectTable = table('project') - .from('Project') +export const articleTable = table('article') + .from('Article') .columns({ id: string(), name: string(), - description: string().optional(), - status: enumeration(), - priority: enumeration(), - startDate: number().from('start_date').optional(), - endDate: number().from('end_date').optional(), - budget: number().optional(), - createdAt: number(), - organizationId: string().from('organization_id'), }) .primaryKey('id'); @@ -329,33 +140,14 @@ export const tagTable = table('tag') .columns({ id: string(), name: string(), - color: string(), - createdAt: number(), - }) - .primaryKey('id'); - -export const articleTable = table('article') - .from('Article') - .columns({ - id: string(), - title: string(), - slug: string(), - content: string(), - excerpt: string().optional(), - publishedAt: number().from('published_at').optional(), - createdAt: number(), - updatedAt: number(), }) .primaryKey('id'); -export const categoryTable = table('category') - .from('Category') +export const workerTable = table('worker') + .from('Worker') .columns({ id: string(), name: string(), - slug: string(), - description: string().optional(), - parentId: string().from('parent_id').optional(), }) .primaryKey('id'); @@ -364,98 +156,50 @@ export const skillTable = table('skill') .columns({ id: string(), name: string(), - description: string().optional(), - category: string().optional(), - }) - .primaryKey('id'); - -export const workerTable = table('worker') - .from('Worker') - .columns({ - id: string(), - name: string(), - email: string(), - hourlyRate: number().from('hourly_rate'), }) .primaryKey('id'); -export const workerSkillTable = table('workerSkills') - .from('worker_skills') +export const workerSkillTable = table('workerSkill') + .from('WorkerSkill') .columns({ id: string(), proficiency: number(), - yearsExperience: number().from('years_experience'), - certified: boolean(), - certifiedAt: number().from('certified_at').optional(), - workerId: string().from('worker_id'), - skillId: string().from('skill_id'), - }) - .primaryKey('id'); - -export const commentTable = table('comment') - .from('Comment') - .columns({ - id: string(), - content: string(), - createdAt: number(), - updatedAt: number(), - articleId: string().from('article_id'), - parentId: string().from('parent_id').optional(), + workerId: string(), + skillId: string(), }) .primaryKey('id'); -export const treeNodeTable = table('treeNodes') - .from('tree_nodes') +export const categoryTable = table('category') + .from('Category') .columns({ id: string(), name: string(), - level: number(), - path: string(), - createdAt: number(), - parentId: string().from('parent_id').optional(), + parentId: string().optional(), }) .primaryKey('id'); -export const socialUserTable = table('socialUsers') - .from('social_users') +export const socialUserTable = table('socialUser') + .from('SocialUser') .columns({ id: string(), username: string(), - name: string(), - bio: string().optional(), - createdAt: number(), }) .primaryKey('id'); -export const followTable = table('follow') - .from('Follow') +export const tenantTable = table('tenant') + .from('Tenant') .columns({ id: string(), - followedAt: number().from('followed_at'), - followerId: string().from('follower_id'), - followingId: string().from('following_id'), - }) - .primaryKey('id'); - -export const compositePkParentTable = table('compositePkParents') - .from('composite_pk_parents') - .columns({ - orgId: string(), - recordId: string(), name: string(), - createdAt: number(), }) - .primaryKey('orgId', 'recordId'); + .primaryKey('id'); -export const compositeFkChildTable = table('compositeFkChildren') - .from('composite_fk_children') +export const tenantConfigTable = table('tenantConfig') + .from('TenantConfig') .columns({ id: string(), - name: string(), - value: number(), - createdAt: number(), - parentOrgId: string().from('parent_org_id'), - parentRecordId: string().from('parent_record_id'), + settings: json(), + tenantId: string(), }) .primaryKey('id'); @@ -464,220 +208,58 @@ export const taskTable = table('task') .columns({ id: string(), title: string(), - description: string().optional(), - status: enumeration(), - priority: enumeration(), - dueDate: number().from('due_date').optional(), - completedAt: number().from('completed_at').optional(), - createdAt: number(), - updatedAt: number(), - projectId: string().from('project_id'), - assigneeId: string().from('assignee_id').optional(), - creatorId: string().from('creator_id'), - }) - .primaryKey('id'); - -export const nativeTypesTable = table('nativeTypes') - .from('native_types') - .columns({ - id: string(), - varcharField: string(), - charField: string(), - textField: string(), - smallIntField: number(), - integerField: number(), - bigIntField: number(), - realField: number(), - doublePrecision: number(), - decimalField: number(), - moneyField: number(), - timestampField: number(), - timestampTz: number(), - dateField: number(), - timeField: number(), - timeTzField: number(), - boolField: boolean(), - byteaField: string(), - jsonField: json(), - jsonbField: json(), - uuidField: string(), - xmlField: string(), - inetField: string(), - }) - .primaryKey('id'); - -export const allOptionalTable = table('allOptional') - .from('AllOptional') - .columns({ - id: string(), - optString: string().optional(), - optInt: number().optional(), - optFloat: number().optional(), - optBool: boolean().optional(), - optDateTime: number().optional(), - optJson: json().optional(), - optBigInt: number().optional(), - optDecimal: number().optional(), - optBytes: string().optional(), - optEnum: enumeration().optional(), - optEnumArr: json(), - }) - .primaryKey('id'); - -export const longFieldNamesTable = table('longFieldNames') - .from('long_field_names') - .columns({ - id: string(), - thisIsAVeryLongFieldNameThatMightCauseIssuesInSomeDatabases: - string().from('long_field_1'), - anotherExtremelyLongFieldNameForTestingPurposesAndEdgeCases: - number().from('long_field_2'), - yetAnotherLongFieldNameToEnsureTheGeneratorHandlesThemCorrectly: - boolean().from('long_field_3'), - }) - .primaryKey('id'); - -export const complexJsonTable = table('complexJson') - .from('complex_json') - .columns({ - id: string(), - userPreferences: json(), - formData: json(), - apiResponse: json(), - geoLocation: json(), - metadata: json().optional(), + creatorId: string(), + assigneeId: string().optional(), }) .primaryKey('id'); -export const orderTable = table('order') - .from('Order') +export const memberTable = table('member') + .from('Member') .columns({ id: string(), - orderNumber: string().from('order_number'), - status: enumeration(), - subtotal: number(), - tax: number(), - shipping: number(), - total: number(), - currency: string(), - notes: string().optional(), - shippingAddress: json().from('shipping_address'), - billingAddress: json().from('billing_address'), - createdAt: number(), - updatedAt: number(), + name: string(), }) .primaryKey('id'); -export const orderLineItemTable = table('orderLineItems') - .from('order_line_items') +export const enumFieldsTable = table('enumFields') + .from('EnumFields') .columns({ id: string(), - productId: string().from('product_id'), - productName: string().from('product_name'), - sku: string(), - quantity: number(), - unitPrice: number().from('unit_price'), - totalPrice: number().from('total_price'), - orderId: string().from('order_id'), + role: enumeration(), + statuses: json(), }) .primaryKey('id'); -export const orderStatusHistoryTable = table('orderStatusHistory') - .from('order_status_history') +export const nativeTypesTable = table('nativeTypes') + .from('NativeTypes') .columns({ id: string(), - fromStatus: enumeration().from('from_status').optional(), - toStatus: enumeration().from('to_status'), - reason: string().optional(), - changedAt: number().from('changed_at'), - changedBy: string().from('changed_by').optional(), - orderId: string().from('order_id'), + varchar: string(), + text: string(), + smallInt: number(), + decimal: number(), + timestamp: number(), + jsonb: json(), }) .primaryKey('id'); -export const notificationTable = table('notification') - .from('Notification') +export const minimalModelTable = table('minimalModel') + .from('MinimalModel') .columns({ id: string(), - title: string(), - body: string(), - data: json().optional(), - read: boolean(), - channels: json(), - sentVia: json(), - priority: enumeration(), - scheduledFor: number().from('scheduled_for').optional(), - sentAt: number().from('sent_at').optional(), - readAt: number().from('read_at').optional(), - createdAt: number(), }) .primaryKey('id'); export const reservedWordsTable = table('reservedWords') - .from('reserved_words') + .from('ReservedWords') .columns({ id: string(), select: string().from('select_field'), from: string().from('from_field'), where: string().from('where_field'), - order: string().from('order_field'), - group: string().from('group_field'), - having: string().from('having_field'), - limit: number().from('limit_field'), - offset: number().from('offset_field'), - join: string().from('join_field'), - insert: string().from('insert_field'), - update: string().from('update_field'), - delete: string().from('delete_field'), - create: string().from('create_field'), - drop: string().from('drop_field'), - table: string().from('table_field'), - index: string().from('index_field'), - }) - .primaryKey('id'); - -export const model123WithNumbers456Table = table('model123WithNumbers456') - .from('Model123WithNumbers456') - .columns({ - id: string(), - field1: string(), - field2: number(), - field3: boolean(), - }) - .primaryKey('id'); - -export const minimalModelTable = table('minimalModel') - .from('MinimalModel') - .columns({ - id: string(), - }) - .primaryKey('id'); - -export const autoGeneratedOnlyTable = table('autoGeneratedOnly') - .from('AutoGeneratedOnly') - .columns({ - id: string(), - createdAt: number(), - updatedAt: number(), }) .primaryKey('id'); -export const _projectToTagTable = table('_projectToTag') - .from('_ProjectToTag') - .columns({ - A: string(), - B: string(), - }) - .primaryKey('A', 'B'); - -export const _projectSkillsTable = table('_projectSkills') - .from('_ProjectSkills') - .columns({ - A: string(), - B: string(), - }) - .primaryKey('A', 'B'); - export const _articleToTagTable = table('_articleToTag') .from('_ArticleToTag') .columns({ @@ -686,414 +268,112 @@ export const _articleToTagTable = table('_articleToTag') }) .primaryKey('A', 'B'); -export const _articleToCategoryTable = table('_articleToCategory') - .from('_ArticleToCategory') - .columns({ - A: string(), - B: string(), - }) - .primaryKey('A', 'B'); - -export const _blockedUsersTable = table('_blockedUsers') - .from('_BlockedUsers') +export const _blockListTable = table('_blockList') + .from('_BlockList') .columns({ A: string(), B: string(), }) .primaryKey('A', 'B'); -export const fieldMappingTableRelationships = relationships( - fieldMappingTable, - ({one}) => ({ - organization: one({ - sourceField: ['organizationId'], - destField: ['id'], - destSchema: organizationTable, - }), - }), -); -export const userAccountTableRelationships = relationships( - userAccountTable, +export const userTableRelationships = relationships( + userTable, ({one, many}) => ({ profile: one({ sourceField: ['id'], destField: ['userId'], - destSchema: userProfileTable, + destSchema: profileTable, }), - sessions: many({ + posts: many({ sourceField: ['id'], - destField: ['userId'], - destSchema: userSessionTable, + destField: ['authorId'], + destSchema: postTable, }), }), ); -export const userProfileTableRelationships = relationships( - userProfileTable, +export const profileTableRelationships = relationships( + profileTable, ({one}) => ({ user: one({ sourceField: ['userId'], destField: ['id'], - destSchema: userAccountTable, + destSchema: userTable, }), }), ); -export const userSessionTableRelationships = relationships( - userSessionTable, - ({one}) => ({ - user: one({ - sourceField: ['userId'], +export const postTableRelationships = relationships( + postTable, + ({one, many}) => ({ + author: one({ + sourceField: ['authorId'], destField: ['id'], - destSchema: userAccountTable, + destSchema: userTable, }), - }), -); -export const tenantUserTableRelationships = relationships( - tenantUserTable, - ({one}) => ({ - tenant: one({ - sourceField: ['tenantId'], - destField: ['id'], - destSchema: tenantTable, + comments: many({ + sourceField: ['id'], + destField: ['postId'], + destSchema: commentTable, }), }), ); -export const tenantApiKeyTableRelationships = relationships( - tenantApiKeyTable, +export const commentTableRelationships = relationships( + commentTable, ({one}) => ({ - tenant: one({ - sourceField: ['tenantId'], + post: one({ + sourceField: ['postId'], destField: ['id'], - destSchema: tenantTable, + destSchema: postTable, }), }), ); -export const tenantTableRelationships = relationships( - tenantTable, +export const articleTableRelationships = relationships( + articleTable, ({many}) => ({ - users: many({ + tags: many( + { + sourceField: ['id'], + destField: ['A'], + destSchema: _articleToTagTable, + }, + { + sourceField: ['B'], + destField: ['id'], + destSchema: tagTable, + }, + ), + }), +); +export const tagTableRelationships = relationships(tagTable, ({many}) => ({ + articles: many( + { sourceField: ['id'], - destField: ['tenantId'], - destSchema: tenantUserTable, - }), - apiKeys: many({ + destField: ['B'], + destSchema: _articleToTagTable, + }, + { + sourceField: ['A'], + destField: ['id'], + destSchema: articleTable, + }, + ), +})); +export const workerTableRelationships = relationships( + workerTable, + ({many}) => ({ + skills: many({ sourceField: ['id'], - destField: ['tenantId'], - destSchema: tenantApiKeyTable, + destField: ['workerId'], + destSchema: workerSkillTable, }), }), ); -export const personTableRelationships = relationships(personTable, ({one}) => ({ - passport: one({ - sourceField: ['id'], - destField: ['personId'], - destSchema: passportTable, - }), - driverLicense: one({ - sourceField: ['id'], - destField: ['personId'], - destSchema: driverLicenseTable, - }), - spouse: one({ - sourceField: ['spouseId'], - destField: ['id'], - destSchema: personTable, - }), - spouseOf: one({ - sourceField: ['id'], - destField: ['spouseId'], - destSchema: personTable, - }), -})); -export const passportTableRelationships = relationships( - passportTable, - ({one}) => ({ - person: one({ - sourceField: ['personId'], - destField: ['id'], - destSchema: personTable, - }), - }), -); -export const driverLicenseTableRelationships = relationships( - driverLicenseTable, - ({one}) => ({ - person: one({ - sourceField: ['personId'], - destField: ['id'], - destSchema: personTable, - }), - }), -); -export const organizationTableRelationships = relationships( - organizationTable, - ({many}) => ({ - departments: many({ - sourceField: ['id'], - destField: ['organizationId'], - destSchema: departmentTable, - }), - employees: many({ - sourceField: ['id'], - destField: ['organizationId'], - destSchema: employeeTable, - }), - projects: many({ - sourceField: ['id'], - destField: ['organizationId'], - destSchema: projectTable, - }), - fieldMappings: many({ - sourceField: ['id'], - destField: ['organizationId'], - destSchema: fieldMappingTable, - }), - }), -); -export const departmentTableRelationships = relationships( - departmentTable, - ({one, many}) => ({ - organization: one({ - sourceField: ['organizationId'], - destField: ['id'], - destSchema: organizationTable, - }), - employees: many({ - sourceField: ['id'], - destField: ['departmentId'], - destSchema: employeeTable, - }), - teams: many({ - sourceField: ['id'], - destField: ['departmentId'], - destSchema: teamTable, - }), - }), -); -export const employeeTableRelationships = relationships( - employeeTable, - ({one, many}) => ({ - organization: one({ - sourceField: ['organizationId'], - destField: ['id'], - destSchema: organizationTable, - }), - department: one({ - sourceField: ['departmentId'], - destField: ['id'], - destSchema: departmentTable, - }), - managedTeam: one({ - sourceField: ['id'], - destField: ['managerId'], - destSchema: teamTable, - }), - teamMemberships: many({ - sourceField: ['id'], - destField: ['employeeId'], - destSchema: teamMemberTable, - }), - assignedTasks: many({ - sourceField: ['id'], - destField: ['assigneeId'], - destSchema: taskTable, - }), - createdTasks: many({ - sourceField: ['id'], - destField: ['creatorId'], - destSchema: taskTable, - }), - }), -); -export const teamTableRelationships = relationships( - teamTable, - ({one, many}) => ({ - department: one({ - sourceField: ['departmentId'], - destField: ['id'], - destSchema: departmentTable, - }), - manager: one({ - sourceField: ['managerId'], - destField: ['id'], - destSchema: employeeTable, - }), - members: many({ - sourceField: ['id'], - destField: ['teamId'], - destSchema: teamMemberTable, - }), - }), -); -export const teamMemberTableRelationships = relationships( - teamMemberTable, - ({one}) => ({ - team: one({ - sourceField: ['teamId'], - destField: ['id'], - destSchema: teamTable, - }), - employee: one({ - sourceField: ['employeeId'], - destField: ['id'], - destSchema: employeeTable, - }), - }), -); -export const projectTableRelationships = relationships( - projectTable, - ({one, many}) => ({ - organization: one({ - sourceField: ['organizationId'], - destField: ['id'], - destSchema: organizationTable, - }), - tags: many( - { - sourceField: ['id'], - destField: ['A'], - destSchema: _projectToTagTable, - }, - { - sourceField: ['B'], - destField: ['id'], - destSchema: tagTable, - }, - ), - tasks: many({ - sourceField: ['id'], - destField: ['projectId'], - destSchema: taskTable, - }), - requiredSkills: many( - { - sourceField: ['id'], - destField: ['A'], - destSchema: _projectSkillsTable, - }, - { - sourceField: ['B'], - destField: ['id'], - destSchema: skillTable, - }, - ), - }), -); -export const tagTableRelationships = relationships(tagTable, ({many}) => ({ - projects: many( - { - sourceField: ['id'], - destField: ['B'], - destSchema: _projectToTagTable, - }, - { - sourceField: ['A'], - destField: ['id'], - destSchema: projectTable, - }, - ), - articles: many( - { - sourceField: ['id'], - destField: ['B'], - destSchema: _articleToTagTable, - }, - { - sourceField: ['A'], - destField: ['id'], - destSchema: articleTable, - }, - ), -})); -export const articleTableRelationships = relationships( - articleTable, - ({many}) => ({ - tags: many( - { - sourceField: ['id'], - destField: ['A'], - destSchema: _articleToTagTable, - }, - { - sourceField: ['B'], - destField: ['id'], - destSchema: tagTable, - }, - ), - categories: many( - { - sourceField: ['id'], - destField: ['A'], - destSchema: _articleToCategoryTable, - }, - { - sourceField: ['B'], - destField: ['id'], - destSchema: categoryTable, - }, - ), - comments: many({ - sourceField: ['id'], - destField: ['articleId'], - destSchema: commentTable, - }), - }), -); -export const categoryTableRelationships = relationships( - categoryTable, - ({one, many}) => ({ - parent: one({ - sourceField: ['parentId'], - destField: ['id'], - destSchema: categoryTable, - }), - children: many({ - sourceField: ['id'], - destField: ['parentId'], - destSchema: categoryTable, - }), - articles: many( - { - sourceField: ['id'], - destField: ['B'], - destSchema: _articleToCategoryTable, - }, - { - sourceField: ['A'], - destField: ['id'], - destSchema: articleTable, - }, - ), - }), -); -export const skillTableRelationships = relationships(skillTable, ({many}) => ({ - workers: many({ +export const skillTableRelationships = relationships(skillTable, ({many}) => ({ + workers: many({ sourceField: ['id'], destField: ['skillId'], destSchema: workerSkillTable, }), - projects: many( - { - sourceField: ['id'], - destField: ['B'], - destSchema: _projectSkillsTable, - }, - { - sourceField: ['A'], - destField: ['id'], - destSchema: projectTable, - }, - ), })); -export const workerTableRelationships = relationships( - workerTable, - ({many}) => ({ - skills: many({ - sourceField: ['id'], - destField: ['workerId'], - destSchema: workerSkillTable, - }), - }), -); export const workerSkillTableRelationships = relationships( workerSkillTable, ({one}) => ({ @@ -1109,59 +389,29 @@ export const workerSkillTableRelationships = relationships( }), }), ); -export const commentTableRelationships = relationships( - commentTable, - ({one, many}) => ({ - article: one({ - sourceField: ['articleId'], - destField: ['id'], - destSchema: articleTable, - }), - parent: one({ - sourceField: ['parentId'], - destField: ['id'], - destSchema: commentTable, - }), - replies: many({ - sourceField: ['id'], - destField: ['parentId'], - destSchema: commentTable, - }), - }), -); -export const treeNodeTableRelationships = relationships( - treeNodeTable, +export const categoryTableRelationships = relationships( + categoryTable, ({one, many}) => ({ parent: one({ sourceField: ['parentId'], destField: ['id'], - destSchema: treeNodeTable, + destSchema: categoryTable, }), children: many({ sourceField: ['id'], destField: ['parentId'], - destSchema: treeNodeTable, + destSchema: categoryTable, }), }), ); export const socialUserTableRelationships = relationships( socialUserTable, ({many}) => ({ - following: many({ - sourceField: ['id'], - destField: ['followerId'], - destSchema: followTable, - }), - followers: many({ - sourceField: ['id'], - destField: ['followingId'], - destSchema: followTable, - }), - blockedUsers: many( + blocked: many( { sourceField: ['id'], destField: ['A'], - destSchema: _blockedUsersTable, + destSchema: _blockListTable, }, { sourceField: ['B'], @@ -1173,7 +423,7 @@ export const socialUserTableRelationships = relationships( { sourceField: ['id'], destField: ['A'], - destSchema: _blockedUsersTable, + destSchema: _blockListTable, }, { sourceField: ['B'], @@ -1183,114 +433,47 @@ export const socialUserTableRelationships = relationships( ), }), ); -export const followTableRelationships = relationships(followTable, ({one}) => ({ - follower: one({ - sourceField: ['followerId'], - destField: ['id'], - destSchema: socialUserTable, - }), - following: one({ - sourceField: ['followingId'], - destField: ['id'], - destSchema: socialUserTable, +export const tenantTableRelationships = relationships(tenantTable, ({one}) => ({ + config: one({ + sourceField: ['id'], + destField: ['tenantId'], + destSchema: tenantConfigTable, }), })); -export const compositePkParentTableRelationships = relationships( - compositePkParentTable, - ({many}) => ({ - children: many({ - sourceField: ['orgId', 'recordId'], - destField: ['parentOrgId', 'parentRecordId'], - destSchema: compositeFkChildTable, - }), - }), -); -export const compositeFkChildTableRelationships = relationships( - compositeFkChildTable, +export const tenantConfigTableRelationships = relationships( + tenantConfigTable, ({one}) => ({ - parent: one({ - sourceField: ['parentOrgId', 'parentRecordId'], - destField: ['orgId', 'recordId'], - destSchema: compositePkParentTable, + tenant: one({ + sourceField: ['tenantId'], + destField: ['id'], + destSchema: tenantTable, }), }), ); export const taskTableRelationships = relationships(taskTable, ({one}) => ({ - project: one({ - sourceField: ['projectId'], + creator: one({ + sourceField: ['creatorId'], destField: ['id'], - destSchema: projectTable, + destSchema: memberTable, }), assignee: one({ sourceField: ['assigneeId'], destField: ['id'], - destSchema: employeeTable, - }), - creator: one({ - sourceField: ['creatorId'], - destField: ['id'], - destSchema: employeeTable, + destSchema: memberTable, }), })); -export const orderTableRelationships = relationships(orderTable, ({many}) => ({ - lineItems: many({ - sourceField: ['id'], - destField: ['orderId'], - destSchema: orderLineItemTable, - }), - statusHistory: many({ - sourceField: ['id'], - destField: ['orderId'], - destSchema: orderStatusHistoryTable, - }), -})); -export const orderLineItemTableRelationships = relationships( - orderLineItemTable, - ({one}) => ({ - order: one({ - sourceField: ['orderId'], - destField: ['id'], - destSchema: orderTable, - }), - }), -); -export const orderStatusHistoryTableRelationships = relationships( - orderStatusHistoryTable, - ({one}) => ({ - order: one({ - sourceField: ['orderId'], - destField: ['id'], - destSchema: orderTable, - }), - }), -); -export const _projectToTagTableRelationships = relationships( - _projectToTagTable, - ({one}) => ({ - modelA: one({ - sourceField: ['A'], - destField: ['id'], - destSchema: projectTable, - }), - modelB: one({ - sourceField: ['B'], - destField: ['id'], - destSchema: tagTable, - }), - }), -); -export const _projectSkillsTableRelationships = relationships( - _projectSkillsTable, - ({one}) => ({ - modelA: one({ - sourceField: ['A'], - destField: ['id'], - destSchema: projectTable, +export const memberTableRelationships = relationships( + memberTable, + ({many}) => ({ + createdTasks: many({ + sourceField: ['id'], + destField: ['creatorId'], + destSchema: taskTable, }), - modelB: one({ - sourceField: ['B'], - destField: ['id'], - destSchema: skillTable, + assignedTasks: many({ + sourceField: ['id'], + destField: ['assigneeId'], + destSchema: taskTable, }), }), ); @@ -1309,23 +492,8 @@ export const _articleToTagTableRelationships = relationships( }), }), ); -export const _articleToCategoryTableRelationships = relationships( - _articleToCategoryTable, - ({one}) => ({ - modelA: one({ - sourceField: ['A'], - destField: ['id'], - destSchema: articleTable, - }), - modelB: one({ - sourceField: ['B'], - destField: ['id'], - destSchema: categoryTable, - }), - }), -); -export const _blockedUsersTableRelationships = relationships( - _blockedUsersTable, +export const _blockListTableRelationships = relationships( + _blockListTable, ({one}) => ({ modelA: one({ sourceField: ['A'], @@ -1345,96 +513,53 @@ export const _blockedUsersTableRelationships = relationships( */ export const schema = createSchema({ tables: [ - allScalarTypesTable, - allArrayTypesTable, - defaultValuesTable, + scalarTypesTable, + optionalTypesTable, + arrayTypesTable, fieldMappingTable, - userAccountTable, - userProfileTable, - userSessionTable, - tenantUserTable, - tenantApiKeyTable, - tenantTable, - uniqueConstraintsTable, - indexedModelTable, - personTable, - passportTable, - driverLicenseTable, - organizationTable, - departmentTable, - employeeTable, - teamTable, - teamMemberTable, - projectTable, - tagTable, + tableMappingTable, + combinedMappingTable, + compositePkTable, + userTable, + profileTable, + postTable, + commentTable, articleTable, - categoryTable, - skillTable, + tagTable, workerTable, + skillTable, workerSkillTable, - commentTable, - treeNodeTable, + categoryTable, socialUserTable, - followTable, - compositePkParentTable, - compositeFkChildTable, + tenantTable, + tenantConfigTable, taskTable, + memberTable, + enumFieldsTable, nativeTypesTable, - allOptionalTable, - longFieldNamesTable, - complexJsonTable, - orderTable, - orderLineItemTable, - orderStatusHistoryTable, - notificationTable, - reservedWordsTable, - model123WithNumbers456Table, minimalModelTable, - autoGeneratedOnlyTable, - _projectToTagTable, - _projectSkillsTable, + reservedWordsTable, _articleToTagTable, - _articleToCategoryTable, - _blockedUsersTable, + _blockListTable, ], relationships: [ - fieldMappingTableRelationships, - userAccountTableRelationships, - userProfileTableRelationships, - userSessionTableRelationships, - tenantUserTableRelationships, - tenantApiKeyTableRelationships, - tenantTableRelationships, - personTableRelationships, - passportTableRelationships, - driverLicenseTableRelationships, - organizationTableRelationships, - departmentTableRelationships, - employeeTableRelationships, - teamTableRelationships, - teamMemberTableRelationships, - projectTableRelationships, - tagTableRelationships, + userTableRelationships, + profileTableRelationships, + postTableRelationships, + commentTableRelationships, articleTableRelationships, - categoryTableRelationships, - skillTableRelationships, + tagTableRelationships, workerTableRelationships, + skillTableRelationships, workerSkillTableRelationships, - commentTableRelationships, - treeNodeTableRelationships, + categoryTableRelationships, socialUserTableRelationships, - followTableRelationships, - compositePkParentTableRelationships, - compositeFkChildTableRelationships, + tenantTableRelationships, + tenantConfigTableRelationships, taskTableRelationships, - orderTableRelationships, - orderLineItemTableRelationships, - orderStatusHistoryTableRelationships, - _projectToTagTableRelationships, - _projectSkillsTableRelationships, + memberTableRelationships, _articleToTagTableRelationships, - _articleToCategoryTableRelationships, - _blockedUsersTableRelationships, + _blockListTableRelationships, ], }); diff --git a/integration/schema.prisma b/integration/schema.prisma index 37b6067..443ae85 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -7,985 +7,310 @@ generator zero { output = "./generated/zero" prettier = true camelCase = true - excludeTables = ["IgnoredModel", "InternalAuditLog"] + excludeTables = ["ExcludedModel"] } // ============================================================================ -// ENUMS - All enum variations +// ENUMS // ============================================================================ -/// Basic enum +/// TEST: Basic enum → TypeScript union type enum Role { USER ADMIN - MODERATOR - SUPER_ADMIN } -/// Enum with database name mapping +/// TEST: Enum with @map → uses dbName in union values enum Status { - PENDING @map("pending") - ACTIVE @map("active") - INACTIVE @map("inactive") - ARCHIVED @map("archived") - DELETED @map("deleted") -} - -/// Enum for order status -enum OrderStatus { - DRAFT - SUBMITTED - PROCESSING - SHIPPED - DELIVERED - CANCELLED - REFUNDED -} - -/// Enum for priority levels -enum Priority { - LOW - MEDIUM - HIGH - CRITICAL -} - -/// Enum for notification types -enum NotificationType { - EMAIL - SMS - PUSH - IN_APP - WEBHOOK -} - -// ============================================================================ -// MODELS WITH ALL SCALAR TYPES -// ============================================================================ - -/// Model demonstrating ALL scalar types available in Prisma -model AllScalarTypes { - id String @id @default(uuid()) - - // Basic types - stringField String - intField Int - floatField Float - booleanField Boolean - dateTimeField DateTime - jsonField Json - bigIntField BigInt - decimalField Decimal - bytesField Bytes - - // Optional versions of all types - optionalString String? - optionalInt Int? - optionalFloat Float? - optionalBoolean Boolean? - optionalDateTime DateTime? - optionalJson Json? - optionalBigInt BigInt? - optionalDecimal Decimal? - optionalBytes Bytes? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -/// Model demonstrating ALL array types -model AllArrayTypes { - id String @id @default(uuid()) - - // Array types (PostgreSQL supports these natively) - stringArray String[] - intArray Int[] - floatArray Float[] - booleanArray Boolean[] - dateTimeArray DateTime[] - jsonArray Json[] - bigIntArray BigInt[] - decimalArray Decimal[] - bytesArray Bytes[] - - // Enum arrays - roles Role[] - statuses Status[] - notificationTypes NotificationType[] - - createdAt DateTime @default(now()) + ACTIVE @map("active") + INACTIVE @map("inactive") } // ============================================================================ -// MODELS WITH VARIOUS DEFAULT VALUES +// SCALAR TYPES - Tests all Prisma scalar → Zero type mappings // ============================================================================ -/// Model demonstrating all @default variations -model DefaultValues { - // UUID default (most common) - id String @id @default(uuid()) - - // Auto-increment (for Int IDs) - sequenceNum Int @unique @default(autoincrement()) - - // CUID default - cuidField String @unique @default(cuid()) - - // Static defaults - defaultString String @default("default_value") - defaultInt Int @default(0) - defaultFloat Float @default(0.0) - defaultBool Boolean @default(false) - defaultTrue Boolean @default(true) - - // Database-generated defaults - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Enum with default - status Status @default(PENDING) - role Role @default(USER) - - // JSON with default - metadata Json @default("{}") - settings Json @default("{\"theme\": \"dark\", \"notifications\": true}") - - // Array with default - tags String[] @default([]) +/// TEST: All scalar types and their Zero mappings +/// - String → string() +/// - Int/Float/BigInt/Decimal → number() +/// - Boolean → boolean() +/// - DateTime → number() (timestamp) +/// - Json → json() +/// - Bytes → string() (fallback) +model ScalarTypes { + id String @id @default(uuid()) + str String + int Int + float Float + bool Boolean + dateTime DateTime + json Json + bigInt BigInt + decimal Decimal + bytes Bytes +} + +/// TEST: Optional modifier → .optional() on all types +model OptionalTypes { + id String @id @default(uuid()) + str String? + int Int? + dateTime DateTime? + json Json? + enum Status? +} + +/// TEST: Array types → json() since Zero doesn't support native arrays +model ArrayTypes { + id String @id @default(uuid()) + strings String[] + ints Int[] + bools Boolean[] + enums Role[] + jsonArray Json[] } // ============================================================================ -// FIELD MAPPING WITH @map +// FIELD & TABLE MAPPING - Tests @map and @@map attributes // ============================================================================ -/// Model demonstrating field name mapping +/// TEST: Field @map → .from('db_column_name') model FieldMapping { - id String @id @default(uuid()) - - // Simple snake_case to camelCase mapping - firstName String @map("first_name") - lastName String @map("last_name") - emailAddr String @map("email_address") @unique - phoneNumber String @map("phone_number") - - // Mapping with different naming conventions - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - // Boolean with mapping - isActive Boolean @default(true) @map("is_active") - isVerified Boolean @default(false) @map("is_verified") - - // Foreign key with mapping - organizationId String @map("organization_id") - organization Organization @relation(fields: [organizationId], references: [id]) -} - -// ============================================================================ -// TABLE MAPPING WITH @@map -// ============================================================================ - -/// Model with table name mapping -model UserAccount { - id String @id @default(uuid()) - username String @unique - email String @unique - createdAt DateTime @default(now()) - - profile UserProfile? - sessions UserSession[] - - @@map("user_accounts") -} - -/// Another model with table mapping -model UserProfile { - id String @id @default(uuid()) - bio String? - avatarUrl String? @map("avatar_url") - website String? - - userId String @unique @map("user_id") - user UserAccount @relation(fields: [userId], references: [id]) - - @@map("user_profiles") -} - -/// Session model with mapping -model UserSession { - id String @id @default(uuid()) - token String @unique - userAgent String? @map("user_agent") - ipAddress String? @map("ip_address") - lastActiveAt DateTime @map("last_active_at") - expiresAt DateTime @map("expires_at") - - userId String @map("user_id") - user UserAccount @relation(fields: [userId], references: [id]) - - @@map("user_sessions") -} - -// ============================================================================ -// COMPOSITE PRIMARY KEYS (@@id) -// ============================================================================ - -/// Model with composite primary key -model TenantUser { - tenantId String - userId String - role Role @default(USER) - joinedAt DateTime @default(now()) - - tenant Tenant @relation(fields: [tenantId], references: [id]) - - @@id([tenantId, userId]) -} - -/// Another composite key model - for API keys scoped to tenant -model TenantApiKey { - tenantId String - keyId String - name String - hashedKey String @map("hashed_key") - permissions String[] - expiresAt DateTime? @map("expires_at") - createdAt DateTime @default(now()) @map("created_at") - - tenant Tenant @relation(fields: [tenantId], references: [id]) - - @@id([tenantId, keyId]) - @@map("tenant_api_keys") + id String @id @default(uuid()) + firstName String @map("first_name") + lastName String @map("last_name") } -/// Tenant model -model Tenant { - id String @id @default(uuid()) - name String - slug String @unique - createdAt DateTime @default(now()) +/// TEST: Table @@map → table().from('db_table_name') +model TableMapping { + id String @id @default(uuid()) + name String - users TenantUser[] - apiKeys TenantApiKey[] + @@map("table_mappings") } -// ============================================================================ -// COMPOSITE UNIQUE CONSTRAINTS (@@unique) -// ============================================================================ - -/// Model with various unique constraints -model UniqueConstraints { - id String @id @default(uuid()) +/// TEST: Combined field + table mapping +model CombinedMapping { + id String @id @default(uuid()) + createdAt DateTime @map("created_at") - // Single field unique - email String @unique - username String @unique - - // Fields for composite unique - orgId String @map("org_id") - employeeId String @map("employee_id") - - // Additional fields - firstName String @map("first_name") - lastName String @map("last_name") - - @@unique([orgId, employeeId]) - @@unique([orgId, email]) - @@map("unique_constraints") + @@map("combined_mappings") } // ============================================================================ -// INDEXES (@@index) +// PRIMARY KEYS - Tests @id and @@id // ============================================================================ -/// Model demonstrating various index types -model IndexedModel { - id String @id @default(uuid()) - - // Frequently queried fields - email String - status Status - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") +/// TEST: Composite primary key (@@id) → .primaryKey('field1', 'field2') +model CompositePK { + tenantId String + recordId String + data String - // Foreign keys - categoryId String @map("category_id") - authorId String @map("author_id") - - // Text search fields - title String - content String? - - // Single column indexes - @@index([email]) - @@index([status]) - @@index([createdAt]) - - // Composite indexes - @@index([categoryId, status]) - @@index([authorId, createdAt]) - @@index([status, createdAt]) - - @@map("indexed_models") + @@id([tenantId, recordId]) } // ============================================================================ -// ONE-TO-ONE RELATIONSHIPS +// RELATIONSHIPS - One-to-One // ============================================================================ -/// Person model for 1:1 relationship -model Person { - id String @id @default(uuid()) - email String @unique - name String - createdAt DateTime @default(now()) - - // One-to-one: Person has one Passport - passport Passport? - - // One-to-one: Person has one DriverLicense - driverLicense DriverLicense? - - // One-to-one with same table (spouse) - spouseId String? @unique - spouse Person? @relation("Spouse", fields: [spouseId], references: [id]) - spouseOf Person? @relation("Spouse") -} - -/// Passport - required one-to-one -model Passport { - id String @id @default(uuid()) - passportNumber String @unique @map("passport_number") - country String - expiryDate DateTime @map("expiry_date") - - personId String @unique @map("person_id") - person Person @relation(fields: [personId], references: [id]) +/// TEST: One-to-one relationship (has one side) +model User { + id String @id @default(uuid()) + email String @unique + profile Profile? + posts Post[] } -/// DriverLicense - optional one-to-one -model DriverLicense { - id String @id @default(uuid()) - licenseNumber String @unique @map("license_number") - state String - expiryDate DateTime @map("expiry_date") - class String @default("C") - - personId String @unique @map("person_id") - person Person @relation(fields: [personId], references: [id]) - - @@map("driver_licenses") +/// TEST: One-to-one relationship (belongs to side with FK) +model Profile { + id String @id @default(uuid()) + bio String? + userId String @unique + user User @relation(fields: [userId], references: [id]) } // ============================================================================ -// ONE-TO-MANY RELATIONSHIPS +// RELATIONSHIPS - One-to-Many // ============================================================================ -/// Organization for 1:N relationships -model Organization { - id String @id @default(uuid()) - name String - slug String @unique - description String? - website String? - createdAt DateTime @default(now()) - - // One org has many departments - departments Department[] - - // One org has many employees - employees Employee[] - - // One org has many projects - projects Project[] - - // Field mappings from other models - fieldMappings FieldMapping[] -} - -/// Department belongs to Organization -model Department { - id String @id @default(uuid()) - name String - code String - description String? - budget Decimal? - - organizationId String @map("organization_id") - organization Organization @relation(fields: [organizationId], references: [id]) - - // One department has many employees - employees Employee[] - - // One department has many teams - teams Team[] - - @@unique([organizationId, code]) -} - -/// Employee belongs to Organization and Department -model Employee { - id String @id @default(uuid()) - employeeNo String @unique @map("employee_no") - firstName String @map("first_name") - lastName String @map("last_name") - email String @unique - title String? - salary Decimal? - hireDate DateTime @map("hire_date") - - organizationId String @map("organization_id") - organization Organization @relation(fields: [organizationId], references: [id]) - - departmentId String? @map("department_id") - department Department? @relation(fields: [departmentId], references: [id]) - - // Employee can manage a team - managedTeam Team? @relation("TeamManager") - - // Employee belongs to teams - teamMemberships TeamMember[] - - // Employee has assigned tasks - assignedTasks Task[] @relation("TaskAssignee") - - // Employee created tasks - createdTasks Task[] @relation("TaskCreator") -} - -/// Team belongs to Department -model Team { - id String @id @default(uuid()) - name String - description String? - createdAt DateTime @default(now()) - - departmentId String @map("department_id") - department Department @relation(fields: [departmentId], references: [id]) - - // Team has one manager (Employee) - managerId String? @unique @map("manager_id") - manager Employee? @relation("TeamManager", fields: [managerId], references: [id]) - - // Team has many members through TeamMember - members TeamMember[] +/// TEST: One-to-many (has many side) - uses many() with back-reference FK +model Post { + id String @id @default(uuid()) + title String + authorId String + author User @relation(fields: [authorId], references: [id]) + comments Comment[] } -/// Junction table for Team-Employee (explicit many-to-many with extra fields) -model TeamMember { - id String @id @default(uuid()) - role String @default("member") - joinedAt DateTime @default(now()) @map("joined_at") - - teamId String @map("team_id") - team Team @relation(fields: [teamId], references: [id]) - - employeeId String @map("employee_id") - employee Employee @relation(fields: [employeeId], references: [id]) - - @@unique([teamId, employeeId]) - @@map("team_members") +/// TEST: One-to-many (belongs to side) +model Comment { + id String @id @default(uuid()) + text String + postId String + post Post @relation(fields: [postId], references: [id]) } // ============================================================================ -// MANY-TO-MANY RELATIONSHIPS (IMPLICIT) +// RELATIONSHIPS - Many-to-Many (Implicit) // ============================================================================ -/// Project with implicit M:N to Tag -model Project { - id String @id @default(uuid()) - name String - description String? - status Status @default(PENDING) - priority Priority @default(MEDIUM) - startDate DateTime? @map("start_date") - endDate DateTime? @map("end_date") - budget Decimal? - createdAt DateTime @default(now()) - - organizationId String @map("organization_id") - organization Organization @relation(fields: [organizationId], references: [id]) - - // Implicit many-to-many with Tag - tags Tag[] - - // Project has many tasks - tasks Task[] - - // Implicit many-to-many with Skill (skills required for project) - requiredSkills Skill[] @relation("ProjectSkills") -} - -/// Tag with implicit M:N to Project -model Tag { - id String @id @default(uuid()) - name String @unique - color String @default("#808080") - createdAt DateTime @default(now()) - - // Implicit many-to-many with Project - projects Project[] - - // Implicit many-to-many with Article - articles Article[] -} - -/// Article with implicit M:N to Tag and Category +/// TEST: Implicit M:N → creates _ArticleToTag join table with A/B columns model Article { - id String @id @default(uuid()) - title String - slug String @unique - content String - excerpt String? - publishedAt DateTime? @map("published_at") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Many-to-many with Tag (implicit) + id String @id @default(uuid()) + name String tags Tag[] - - // Many-to-many with Category (implicit) - categories Category[] - - // Article has many comments - comments Comment[] } -/// Category with implicit M:N to Article -model Category { - id String @id @default(uuid()) - name String - slug String @unique - description String? - - // Self-referential: parent category - parentId String? @map("parent_id") - parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id]) - children Category[] @relation("CategoryHierarchy") - - // Many-to-many with Article (implicit) +/// TEST: Implicit M:N (other side) → chained relationship through join table +model Tag { + id String @id @default(uuid()) + name String articles Article[] } // ============================================================================ -// MANY-TO-MANY RELATIONSHIPS (EXPLICIT WITH EXTRA DATA) +// RELATIONSHIPS - Many-to-Many (Explicit with extra data) // ============================================================================ -/// Skill model -model Skill { - id String @id @default(uuid()) - name String @unique - description String? - category String? - - // Explicit M:N with Worker through WorkerSkill - workers WorkerSkill[] - - // Implicit M:N with Project - projects Project[] @relation("ProjectSkills") -} - -/// Worker model +/// TEST: Explicit M:N source - treated as regular 1:N model Worker { - id String @id @default(uuid()) - name String - email String @unique - hourlyRate Decimal @map("hourly_rate") - - // Explicit M:N with Skill through WorkerSkill + id String @id @default(uuid()) + name String skills WorkerSkill[] } -/// Junction table with extra data (proficiency level, years of experience) -model WorkerSkill { - id String @id @default(uuid()) - proficiency Int @default(1) // 1-5 scale - yearsExperience Int @default(0) @map("years_experience") - certified Boolean @default(false) - certifiedAt DateTime? @map("certified_at") - - workerId String @map("worker_id") - worker Worker @relation(fields: [workerId], references: [id]) +/// TEST: Explicit M:N target - treated as regular 1:N +model Skill { + id String @id @default(uuid()) + name String + workers WorkerSkill[] +} - skillId String @map("skill_id") - skill Skill @relation(fields: [skillId], references: [id]) +/// TEST: Explicit junction table with extra fields (proficiency) +model WorkerSkill { + id String @id @default(uuid()) + proficiency Int @default(1) + workerId String + worker Worker @relation(fields: [workerId], references: [id]) + skillId String + skill Skill @relation(fields: [skillId], references: [id]) @@unique([workerId, skillId]) - @@map("worker_skills") } // ============================================================================ -// SELF-REFERENTIAL RELATIONSHIPS +// RELATIONSHIPS - Self-Referential // ============================================================================ -/// Comment with self-referential replies -model Comment { - id String @id @default(uuid()) - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Comment belongs to Article - articleId String @map("article_id") - article Article @relation(fields: [articleId], references: [id]) - - // Self-referential: parent comment (for replies) - parentId String? @map("parent_id") - parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) - replies Comment[] @relation("CommentReplies") -} - -/// Tree node with self-referential parent/children -model TreeNode { - id String @id @default(uuid()) - name String - level Int @default(0) - path String // Materialized path like "/1/2/3" - createdAt DateTime @default(now()) - - parentId String? @map("parent_id") - parent TreeNode? @relation("TreeHierarchy", fields: [parentId], references: [id]) - children TreeNode[] @relation("TreeHierarchy") - - @@map("tree_nodes") +/// TEST: Self-referential 1:N (parent/children tree) +model Category { + id String @id @default(uuid()) + name String + parentId String? + parent Category? @relation("CategoryTree", fields: [parentId], references: [id]) + children Category[] @relation("CategoryTree") } -/// Social graph: User follows User +/// TEST: Self-referential implicit M:N → creates _BlockList join table model SocialUser { - id String @id @default(uuid()) - username String @unique - name String - bio String? - createdAt DateTime @default(now()) - - // Self-referential M:N through Follow junction - following Follow[] @relation("Follower") - followers Follow[] @relation("Following") - - // Self-referential: blocked users (implicit M:N) - blockedUsers SocialUser[] @relation("BlockedUsers") - blockedBy SocialUser[] @relation("BlockedUsers") - - @@map("social_users") -} - -/// Explicit junction for follow relationship with timestamp -model Follow { - id String @id @default(uuid()) - followedAt DateTime @default(now()) @map("followed_at") - - followerId String @map("follower_id") - follower SocialUser @relation("Follower", fields: [followerId], references: [id]) - - followingId String @map("following_id") - following SocialUser @relation("Following", fields: [followingId], references: [id]) - - @@unique([followerId, followingId]) + id String @id @default(uuid()) + username String @unique + blocked SocialUser[] @relation("BlockList") + blockedBy SocialUser[] @relation("BlockList") } // ============================================================================ -// COMPOSITE FOREIGN KEYS +// RELATIONSHIPS - Composite Foreign Keys // ============================================================================ -/// Model with composite primary key for FK reference -model CompositePKParent { - orgId String - recordId String - name String - createdAt DateTime @default(now()) - - // Has many children - children CompositeFKChild[] - - @@id([orgId, recordId]) - @@map("composite_pk_parents") +/// TEST: Parent with composite PK for FK reference +model Tenant { + id String @id @default(uuid()) + name String + config TenantConfig? } -/// Model referencing composite FK -model CompositeFKChild { - id String @id @default(uuid()) - name String - value Int - createdAt DateTime @default(now()) - - // Composite foreign key - parentOrgId String @map("parent_org_id") - parentRecordId String @map("parent_record_id") - - parent CompositePKParent @relation(fields: [parentOrgId, parentRecordId], references: [orgId, recordId]) - - @@index([parentOrgId, parentRecordId]) - @@map("composite_fk_children") +/// TEST: Composite FK relationship +model TenantConfig { + id String @id @default(uuid()) + settings Json + tenantId String @unique + tenant Tenant @relation(fields: [tenantId], references: [id]) } // ============================================================================ -// TASKS WITH MULTIPLE RELATIONSHIPS TO SAME MODEL +// RELATIONSHIPS - Multiple relations to same model // ============================================================================ -/// Task with multiple relations to Employee +/// TEST: Model with multiple relations to same target (creator vs assignee) model Task { - id String @id @default(uuid()) - title String - description String? - status Status @default(PENDING) - priority Priority @default(MEDIUM) - dueDate DateTime? @map("due_date") - completedAt DateTime? @map("completed_at") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Task belongs to Project - projectId String @map("project_id") - project Project @relation(fields: [projectId], references: [id]) - - // Task has assignee (Employee) - can be null - assigneeId String? @map("assignee_id") - assignee Employee? @relation("TaskAssignee", fields: [assigneeId], references: [id]) - - // Task has creator (Employee) - creatorId String @map("creator_id") - creator Employee @relation("TaskCreator", fields: [creatorId], references: [id]) + id String @id @default(uuid()) + title String + creatorId String + creator Member @relation("TaskCreator", fields: [creatorId], references: [id]) + assigneeId String? + assignee Member? @relation("TaskAssignee", fields: [assigneeId], references: [id]) +} + +/// TEST: Target of multiple named relations +model Member { + id String @id @default(uuid()) + name String + createdTasks Task[] @relation("TaskCreator") + assignedTasks Task[] @relation("TaskAssignee") } // ============================================================================ -// NATIVE DATABASE TYPES (@db.*) +// ENUMS IN FIELDS // ============================================================================ -/// Model using PostgreSQL native types -model NativeTypes { - id String @id @default(uuid()) @db.Uuid - - // String variations - varcharField String @db.VarChar(255) - charField String @db.Char(10) - textField String @db.Text - - // Numeric variations - smallIntField Int @db.SmallInt - integerField Int @db.Integer - bigIntField BigInt @db.BigInt - realField Float @db.Real - doublePrecision Float @db.DoublePrecision - decimalField Decimal @db.Decimal(10, 2) - moneyField Decimal @db.Money - - // Date/Time variations - timestampField DateTime @db.Timestamp(6) - timestampTz DateTime @db.Timestamptz(6) - dateField DateTime @db.Date - timeField DateTime @db.Time(6) - timeTzField DateTime @db.Timetz(6) - - // Boolean - boolField Boolean @db.Boolean - - // Binary - byteaField Bytes @db.ByteA - - // JSON variations - jsonField Json @db.Json - jsonbField Json @db.JsonB - - // Special types - uuidField String @db.Uuid - xmlField String @db.Xml - inetField String @db.Inet - - @@map("native_types") +/// TEST: Enum field → enumeration() +/// TEST: Enum array field → json() +model EnumFields { + id String @id @default(uuid()) + role Role + statuses Status[] } // ============================================================================ -// MODELS TO BE EXCLUDED (via excludeTables config) +// NATIVE DATABASE TYPES - Tests @db.* attributes (mapped to base type) // ============================================================================ -/// This model should be excluded from Zero schema -model IgnoredModel { - id String @id @default(uuid()) - name String - secretKey String @map("secret_key") - createdAt DateTime @default(now()) -} - -/// Internal audit log - excluded -model InternalAuditLog { - id String @id @default(uuid()) - action String - tableName String @map("table_name") - recordId String @map("record_id") - oldData Json? @map("old_data") - newData Json? @map("new_data") - userId String? @map("user_id") - ipAddress String? @map("ip_address") - createdAt DateTime @default(now()) - - @@index([tableName, recordId]) - @@index([userId]) - @@map("internal_audit_logs") +/// TEST: Native types → all map to base Zero types +model NativeTypes { + id String @id @default(uuid()) @db.Uuid + varchar String @db.VarChar(255) + text String @db.Text + smallInt Int @db.SmallInt + decimal Decimal @db.Decimal(10, 2) + timestamp DateTime @db.Timestamp(6) + jsonb Json @db.JsonB } // ============================================================================ -// EDGE CASES AND SPECIAL SCENARIOS +// EXCLUDED MODEL - Tests excludeTables config // ============================================================================ -/// Model with all optional fields (except ID) -model AllOptional { - id String @id @default(uuid()) - - optString String? - optInt Int? - optFloat Float? - optBool Boolean? - optDateTime DateTime? - optJson Json? - optBigInt BigInt? - optDecimal Decimal? - optBytes Bytes? - optEnum Status? - optEnumArr Role[] -} - -/// Model with very long field names -model LongFieldNames { - id String @id @default(uuid()) - - thisIsAVeryLongFieldNameThatMightCauseIssuesInSomeDatabases String @map("long_field_1") - anotherExtremelyLongFieldNameForTestingPurposesAndEdgeCases Int @map("long_field_2") - yetAnotherLongFieldNameToEnsureTheGeneratorHandlesThemCorrectly Boolean @map("long_field_3") - - @@map("long_field_names") -} - -/// Model with JSON containing complex nested structure hint -model ComplexJson { - id String @id @default(uuid()) - - // These would benefit from typed JSON in the future - userPreferences Json // { theme: string, language: string, notifications: { email: boolean, push: boolean } } - formData Json // Dynamic form submissions - apiResponse Json // Cached API responses - geoLocation Json // { lat: number, lng: number, accuracy: number } - metadata Json? // Optional complex metadata - - @@map("complex_json") -} - -/// Order model for e-commerce scenario -model Order { - id String @id @default(uuid()) - orderNumber String @unique @map("order_number") - status OrderStatus @default(DRAFT) - subtotal Decimal @db.Decimal(10, 2) - tax Decimal @db.Decimal(10, 2) - shipping Decimal @db.Decimal(10, 2) - total Decimal @db.Decimal(10, 2) - currency String @default("USD") @db.Char(3) - notes String? - - // Shipping address as JSON (denormalized for simplicity) - shippingAddress Json @map("shipping_address") - billingAddress Json @map("billing_address") - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Order has many line items - lineItems OrderLineItem[] - - // Order has many status history entries - statusHistory OrderStatusHistory[] -} - -/// Order line item -model OrderLineItem { - id String @id @default(uuid()) - productId String @map("product_id") - productName String @map("product_name") - sku String - quantity Int - unitPrice Decimal @db.Decimal(10, 2) @map("unit_price") - totalPrice Decimal @db.Decimal(10, 2) @map("total_price") - - orderId String @map("order_id") - order Order @relation(fields: [orderId], references: [id]) - - @@map("order_line_items") -} - -/// Order status history for audit trail -model OrderStatusHistory { - id String @id @default(uuid()) - fromStatus OrderStatus? @map("from_status") - toStatus OrderStatus @map("to_status") - reason String? - changedAt DateTime @default(now()) @map("changed_at") - changedBy String? @map("changed_by") - - orderId String @map("order_id") - order Order @relation(fields: [orderId], references: [id]) - - @@index([orderId, changedAt]) - @@map("order_status_history") -} - -/// Notification model with enum arrays -model Notification { - id String @id @default(uuid()) - title String - body String - data Json? - read Boolean @default(false) - channels NotificationType[] // Which channels to send to - sentVia NotificationType[] // Which channels it was actually sent through - priority Priority @default(MEDIUM) - - scheduledFor DateTime? @map("scheduled_for") - sentAt DateTime? @map("sent_at") - readAt DateTime? @map("read_at") - createdAt DateTime @default(now()) - - @@index([read, createdAt]) -} - -/// Model to test reserved words as field names -model ReservedWords { - id String @id @default(uuid()) - select String @map("select_field") - from String @map("from_field") - where String @map("where_field") - order String @map("order_field") - group String @map("group_field") - having String @map("having_field") - limit Int @map("limit_field") - offset Int @map("offset_field") - join String @map("join_field") - insert String @map("insert_field") - update String @map("update_field") - delete String @map("delete_field") - create String @map("create_field") - drop String @map("drop_field") - table String @map("table_field") - index String @map("index_field") - - @@map("reserved_words") +/// TEST: Model in excludeTables → should NOT appear in generated schema +model ExcludedModel { + id String @id @default(uuid()) + secret String } -/// Model with numbers in name -model Model123WithNumbers456 { - id String @id @default(uuid()) - field1 String - field2 Int - field3 Boolean -} +// ============================================================================ +// EDGE CASES +// ============================================================================ -/// Model testing minimum viable setup +/// TEST: Minimal model (just ID) → basic table generation model MinimalModel { id String @id } -/// Model with only auto-generated fields -model AutoGeneratedOnly { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +/// TEST: Reserved SQL keywords as field names with @map +model ReservedWords { + id String @id @default(uuid()) + select String @map("select_field") + from String @map("from_field") + where String @map("where_field") } diff --git a/src/mappers/schema-mapper.ts b/src/mappers/schema-mapper.ts index d83308d..48871b4 100644 --- a/src/mappers/schema-mapper.ts +++ b/src/mappers/schema-mapper.ts @@ -159,7 +159,10 @@ function mapRelationships( } const backReference = targetModel.fields.find( - f => f.relationName === field.relationName && f.type === model.name, + f => + f.relationName === field.relationName && + f.type === model.name && + f.name !== field.name, // Exclude current field for self-referential relations ); if (field.isList) { @@ -304,7 +307,10 @@ export function transformSchema( if (config.excludeTables?.includes(targetModel.name)) return null; const backReference = targetModel.fields.find( - f => f.relationName === field.relationName && f.type === model.name, + f => + f.relationName === field.relationName && + f.type === model.name && + f.name !== field.name, // Exclude current field for self-referential relations ); if (backReference?.isList) { From addd9cf935ab60b46f6ca32275f0a3c64455e508 Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Thu, 11 Dec 2025 16:19:24 -0700 Subject: [PATCH 3/8] chore: update package json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 735d4bd..ef2aefc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prisma-zero", - "version": "0.1.0-canary.1", + "version": "0.1.0-canary.2", "description": "Generate Zero schemas from Prisma ORM schemas", "type": "module", "scripts": { From c4c506175b71e19199b173906a0f66499b90c306 Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Thu, 11 Dec 2025 16:27:33 -0700 Subject: [PATCH 4/8] chore: remove createCRUDBuilder --- integration/generated/zero/schema.ts | 6 ---- package.json | 1 - pnpm-lock.yaml | 39 -------------------------- src/generators/code-generator.ts | 8 ------ tests/generator.test.ts | 42 ---------------------------- 5 files changed, 96 deletions(-) diff --git a/integration/generated/zero/schema.ts b/integration/generated/zero/schema.ts index 412400b..eee68d3 100644 --- a/integration/generated/zero/schema.ts +++ b/integration/generated/zero/schema.ts @@ -4,7 +4,6 @@ import { boolean, createBuilder, - createCRUDBuilder, createSchema, enumeration, json, @@ -580,11 +579,6 @@ export const zql = createBuilder(schema); * @deprecated Use `zql` instead. */ export const builder = zql; -/** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ -export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module '@rocicorp/zero' { interface DefaultTypes { diff --git a/package.json b/package.json index ef2aefc..d78fa53 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "devDependencies": { "@rocicorp/prettier-config": "^0.4.0", "@rocicorp/zero": "0.24.3000000000", - "@ts-morph/common": "^0.28.1", "@types/node": "^24.10.1", "@types/pg": "^8.15.6", "@types/pluralize": "^0.0.33", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29085d7..6dc0146 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,9 +21,6 @@ importers: '@rocicorp/zero': specifier: 0.24.3000000000 version: 0.24.3000000000(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)) - '@ts-morph/common': - specifier: ^0.28.1 - version: 0.28.1 '@types/node': specifier: ^24.10.1 version: 24.10.2 @@ -525,14 +522,6 @@ packages: peerDependencies: hono: ^4 - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1364,9 +1353,6 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@ts-morph/common@0.28.1': - resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} - '@types/aws-lambda@8.10.152': resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} @@ -2176,10 +2162,6 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -2296,9 +2278,6 @@ packages: parse-prometheus-text-format@1.1.1: resolution: {integrity: sha512-dBlhYVACjRdSqLMFe4/Q1l/Gd3UmXm8ruvsTi7J6ul3ih45AkzkVpI5XHV4aZ37juGZW5+3dGU5lwk+QLM9XJA==} - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3242,12 +3221,6 @@ snapshots: dependencies: hono: 4.10.6 - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4328,12 +4301,6 @@ snapshots: '@standard-schema/spec@1.0.0': {} - '@ts-morph/common@0.28.1': - dependencies: - minimatch: 10.1.1 - path-browserify: 1.0.1 - tinyglobby: 0.2.15 - '@types/aws-lambda@8.10.152': {} '@types/basic-auth@1.1.8': @@ -5212,10 +5179,6 @@ snapshots: mimic-response@3.1.0: {} - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimist@1.2.8: {} mkdirp-classic@0.5.3: {} @@ -5332,8 +5295,6 @@ snapshots: dependencies: shallow-equal: 1.2.1 - path-browserify@1.0.1: {} - path-key@3.1.1: {} path-parse@1.0.7: {} diff --git a/src/generators/code-generator.ts b/src/generators/code-generator.ts index 6152121..629ae38 100644 --- a/src/generators/code-generator.ts +++ b/src/generators/code-generator.ts @@ -13,7 +13,6 @@ function generateImports(schema: TransformedSchema): string { usedImports.add('table'); usedImports.add('createSchema'); usedImports.add('createBuilder'); - usedImports.add('createCRUDBuilder'); // Check which type functions are used in the schema schema.models.forEach(model => { @@ -205,13 +204,6 @@ function generateSchema(schema: TransformedSchema): string { output += ' */\n'; output += 'export const builder = zql;\n'; - output += '/**\n'; - output += ' * Represents the Zero schema CRUD builder.\n'; - output += - ' * This type is auto-generated from your Prisma schema definition.\n'; - output += ' */\n'; - output += 'export const crud = createCRUDBuilder(schema);\n'; - output += '/** Defines the default types for Zero */\n'; output += 'declare module "@rocicorp/zero" {\n'; output += ' interface DefaultTypes {\n'; diff --git a/tests/generator.test.ts b/tests/generator.test.ts index 8bf38ca..2f8b6f7 100644 --- a/tests/generator.test.ts +++ b/tests/generator.test.ts @@ -77,7 +77,6 @@ describe('Generator', () => { import { createBuilder, - createCRUDBuilder, createSchema, number, string, @@ -120,11 +119,6 @@ describe('Generator', () => { * @deprecated Use \`zql\` instead. */ export const builder = zql; - /** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ - export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module "@rocicorp/zero" { interface DefaultTypes { @@ -172,7 +166,6 @@ describe('Generator', () => { import { createBuilder, - createCRUDBuilder, createSchema, enumeration, string, @@ -215,11 +208,6 @@ describe('Generator', () => { * @deprecated Use \`zql\` instead. */ export const builder = zql; - /** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ - export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module "@rocicorp/zero" { interface DefaultTypes { @@ -270,7 +258,6 @@ describe('Generator', () => { import { createBuilder, - createCRUDBuilder, createSchema, relationships, string, @@ -338,11 +325,6 @@ describe('Generator', () => { * @deprecated Use \`zql\` instead. */ export const builder = zql; - /** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ - export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module "@rocicorp/zero" { interface DefaultTypes { @@ -419,7 +401,6 @@ describe('Generator', () => { import { createBuilder, - createCRUDBuilder, createSchema, number, relationships, @@ -517,11 +498,6 @@ describe('Generator', () => { * @deprecated Use \`zql\` instead. */ export const builder = zql; - /** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ - export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module "@rocicorp/zero" { interface DefaultTypes { @@ -570,7 +546,6 @@ describe('Generator', () => { import { createBuilder, - createCRUDBuilder, createSchema, number, relationships, @@ -668,11 +643,6 @@ describe('Generator', () => { * @deprecated Use \`zql\` instead. */ export const builder = zql; - /** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ - export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module "@rocicorp/zero" { interface DefaultTypes { @@ -706,7 +676,6 @@ describe('Generator', () => { import { createBuilder, - createCRUDBuilder, createSchema, json, string, @@ -750,11 +719,6 @@ describe('Generator', () => { * @deprecated Use \`zql\` instead. */ export const builder = zql; - /** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ - export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module "@rocicorp/zero" { interface DefaultTypes { @@ -794,7 +758,6 @@ describe('Generator', () => { import { createBuilder, - createCRUDBuilder, createSchema, json, string, @@ -838,11 +801,6 @@ describe('Generator', () => { * @deprecated Use \`zql\` instead. */ export const builder = zql; - /** - * Represents the Zero schema CRUD builder. - * This type is auto-generated from your Prisma schema definition. - */ - export const crud = createCRUDBuilder(schema); /** Defines the default types for Zero */ declare module "@rocicorp/zero" { interface DefaultTypes { From 9377632f19d1eb33612dd01ca3f7bdb4feb01df1 Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Thu, 11 Dec 2025 16:46:44 -0700 Subject: [PATCH 5/8] chore: fixes --- integration/generated/migration.sql | 335 +++++++++++++++++++++++++++ integration/generated/zero/schema.ts | 4 +- integration/package.json | 3 +- integration/prisma.config.ts | 12 + integration/schema.prisma | 22 +- src/mappers/schema-mapper.ts | 7 +- tests/schema-mapper.test.ts | 50 +++- 7 files changed, 414 insertions(+), 19 deletions(-) create mode 100644 integration/generated/migration.sql create mode 100644 integration/prisma.config.ts diff --git a/integration/generated/migration.sql b/integration/generated/migration.sql new file mode 100644 index 0000000..50a3170 --- /dev/null +++ b/integration/generated/migration.sql @@ -0,0 +1,335 @@ +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "public"; + +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "Status" AS ENUM ('active', 'inactive'); + +-- CreateTable +CREATE TABLE "ScalarTypes" ( + "id" TEXT NOT NULL, + "str" TEXT NOT NULL, + "int" INTEGER NOT NULL, + "float" DOUBLE PRECISION NOT NULL, + "bool" BOOLEAN NOT NULL, + "dateTime" TIMESTAMP(3) NOT NULL, + "json" JSONB NOT NULL, + "bigInt" BIGINT NOT NULL, + "decimal" DECIMAL(65,30) NOT NULL, + "bytes" BYTEA NOT NULL, + + CONSTRAINT "ScalarTypes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OptionalTypes" ( + "id" TEXT NOT NULL, + "str" TEXT, + "int" INTEGER, + "dateTime" TIMESTAMP(3), + "json" JSONB, + "enum" "Status", + + CONSTRAINT "OptionalTypes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ArrayTypes" ( + "id" TEXT NOT NULL, + "strings" TEXT[], + "ints" INTEGER[], + "bools" BOOLEAN[], + "enums" "Role"[], + "jsonArray" JSONB[], + + CONSTRAINT "ArrayTypes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FieldMapping" ( + "id" TEXT NOT NULL, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL, + + CONSTRAINT "FieldMapping_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "table_mappings" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "table_mappings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "combined_mappings" ( + "id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "combined_mappings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CompositePK" ( + "tenantId" TEXT NOT NULL, + "recordId" TEXT NOT NULL, + "data" TEXT NOT NULL, + + CONSTRAINT "CompositePK_pkey" PRIMARY KEY ("tenantId","recordId") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Profile" ( + "id" TEXT NOT NULL, + "bio" TEXT, + "userId" TEXT NOT NULL, + + CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + + CONSTRAINT "Post_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" TEXT NOT NULL, + "text" TEXT NOT NULL, + "postId" TEXT NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Article" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Tag" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Worker" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Worker_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Skill" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Skill_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "WorkerSkill" ( + "id" TEXT NOT NULL, + "proficiency" INTEGER NOT NULL DEFAULT 1, + "workerId" TEXT NOT NULL, + "skillId" TEXT NOT NULL, + + CONSTRAINT "WorkerSkill_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Category" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "parentId" TEXT, + + CONSTRAINT "Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SocialUser" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + + CONSTRAINT "SocialUser_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Tenant" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TenantConfig" ( + "id" TEXT NOT NULL, + "settings" JSONB NOT NULL, + "tenantId" TEXT NOT NULL, + + CONSTRAINT "TenantConfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Task" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "creatorId" TEXT NOT NULL, + "assigneeId" TEXT, + + CONSTRAINT "Task_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Member" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Member_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EnumFields" ( + "id" TEXT NOT NULL, + "role" "Role" NOT NULL, + "statuses" "Status"[], + + CONSTRAINT "EnumFields_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "NativeTypes" ( + "id" UUID NOT NULL, + "varchar" VARCHAR(255) NOT NULL, + "text" TEXT NOT NULL, + "smallInt" SMALLINT NOT NULL, + "decimal" DECIMAL(10,2) NOT NULL, + "timestamp" TIMESTAMP(6) NOT NULL, + "jsonb" JSONB NOT NULL, + + CONSTRAINT "NativeTypes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ExcludedModel" ( + "id" TEXT NOT NULL, + "secret" TEXT NOT NULL, + + CONSTRAINT "ExcludedModel_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MinimalModel" ( + "id" TEXT NOT NULL, + + CONSTRAINT "MinimalModel_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ReservedWords" ( + "id" TEXT NOT NULL, + "select_field" TEXT NOT NULL, + "from_field" TEXT NOT NULL, + "where_field" TEXT NOT NULL, + + CONSTRAINT "ReservedWords_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_ArticleToTag" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_ArticleToTag_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_BlockList" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_BlockList_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WorkerSkill_workerId_skillId_key" ON "WorkerSkill"("workerId", "skillId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SocialUser_username_key" ON "SocialUser"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "TenantConfig_tenantId_key" ON "TenantConfig"("tenantId"); + +-- CreateIndex +CREATE INDEX "_ArticleToTag_B_index" ON "_ArticleToTag"("B"); + +-- CreateIndex +CREATE INDEX "_BlockList_B_index" ON "_BlockList"("B"); + +-- AddForeignKey +ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WorkerSkill" ADD CONSTRAINT "WorkerSkill_workerId_fkey" FOREIGN KEY ("workerId") REFERENCES "Worker"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WorkerSkill" ADD CONSTRAINT "WorkerSkill_skillId_fkey" FOREIGN KEY ("skillId") REFERENCES "Skill"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Category" ADD CONSTRAINT "Category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TenantConfig" ADD CONSTRAINT "TenantConfig_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Task" ADD CONSTRAINT "Task_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Task" ADD CONSTRAINT "Task_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ArticleToTag" ADD CONSTRAINT "_ArticleToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ArticleToTag" ADD CONSTRAINT "_ArticleToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_BlockList" ADD CONSTRAINT "_BlockList_A_fkey" FOREIGN KEY ("A") REFERENCES "SocialUser"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_BlockList" ADD CONSTRAINT "_BlockList_B_fkey" FOREIGN KEY ("B") REFERENCES "SocialUser"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/integration/generated/zero/schema.ts b/integration/generated/zero/schema.ts index eee68d3..0e6113c 100644 --- a/integration/generated/zero/schema.ts +++ b/integration/generated/zero/schema.ts @@ -421,11 +421,11 @@ export const socialUserTableRelationships = relationships( blockedBy: many( { sourceField: ['id'], - destField: ['A'], + destField: ['B'], destSchema: _blockListTable, }, { - sourceField: ['B'], + sourceField: ['A'], destField: ['id'], destSchema: socialUserTable, }, diff --git a/integration/package.json b/integration/package.json index 90c0c06..5e00a30 100644 --- a/integration/package.json +++ b/integration/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "build": "tsc", - "generate": "prisma generate", + "generate": "prisma generate && pnpm migrate", + "migrate": "pnpm prisma migrate diff --from-empty --to-schema schema.prisma --script > generated/migration.sql", "pretest": "pnpm generate", "test": "pnpm build" }, diff --git a/integration/prisma.config.ts b/integration/prisma.config.ts new file mode 100644 index 0000000..6de6a55 --- /dev/null +++ b/integration/prisma.config.ts @@ -0,0 +1,12 @@ +import {defineConfig} from 'prisma/config'; + +const databaseUrl = + process.env.DATABASE_URL ?? + 'postgresql://postgres:postgres@localhost:5432/prisma-zero'; + +export default defineConfig({ + schema: './schema.prisma', + datasource: { + url: databaseUrl, + }, +}); diff --git a/integration/schema.prisma b/integration/schema.prisma index 443ae85..c127c53 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -62,12 +62,12 @@ model OptionalTypes { /// TEST: Array types → json() since Zero doesn't support native arrays model ArrayTypes { - id String @id @default(uuid()) - strings String[] - ints Int[] - bools Boolean[] - enums Role[] - jsonArray Json[] + id String @id @default(uuid()) + strings String[] + ints Int[] + bools Boolean[] + enums Role[] + jsonArray Json[] } // ============================================================================ @@ -91,7 +91,7 @@ model TableMapping { /// TEST: Combined field + table mapping model CombinedMapping { - id String @id @default(uuid()) + id String @id @default(uuid()) createdAt DateTime @map("created_at") @@map("combined_mappings") @@ -124,10 +124,10 @@ model User { /// TEST: One-to-one relationship (belongs to side with FK) model Profile { - id String @id @default(uuid()) + id String @id @default(uuid()) bio String? - userId String @unique - user User @relation(fields: [userId], references: [id]) + userId String @unique + user User @relation(fields: [userId], references: [id]) } // ============================================================================ @@ -226,7 +226,7 @@ model SocialUser { /// TEST: Parent with composite PK for FK reference model Tenant { - id String @id @default(uuid()) + id String @id @default(uuid()) name String config TenantConfig? } diff --git a/src/mappers/schema-mapper.ts b/src/mappers/schema-mapper.ts index 48871b4..2787d09 100644 --- a/src/mappers/schema-mapper.ts +++ b/src/mappers/schema-mapper.ts @@ -182,7 +182,12 @@ function mapRelationships( `Implicit relation ${field.name}: Model ${model.name} or ${targetModel.name} not found.`, ); } - const isModelA = model.name === modelA.name; + const isSelfReferential = model.name === targetModel.name; + const isModelA = isSelfReferential + ? backReference + ? field.name.localeCompare(backReference.name) < 0 + : true + : model.name === modelA.name; // Create a chained relationship through the join table relationships[field.name] = { diff --git a/tests/schema-mapper.test.ts b/tests/schema-mapper.test.ts index 8baf9b0..9241d4e 100644 --- a/tests/schema-mapper.test.ts +++ b/tests/schema-mapper.test.ts @@ -104,10 +104,10 @@ describe('Schema Mapper', () => { const userModel = result.models.find(m => m.tableName === 'User'); expect(userModel).toBeDefined(); if (userModel) { - // Verify that the posts relationship field is not included - expect(userModel.relationships).not.toHaveProperty('posts'); - // Verify that the profile relationship field is still included - expect(userModel.relationships).toHaveProperty('profile'); + // Verify that the posts relationship field is not included + expect(userModel.relationships).not.toHaveProperty('posts'); + // Verify that the profile relationship field is still included + expect(userModel.relationships).toHaveProperty('profile'); } }); }); @@ -332,6 +332,48 @@ describe('Schema Mapper', () => { expect(childrenRelationship).toHaveProperty('destSchema'); } }); + + it('maps self-referential implicit many-to-many relationships to distinct join columns', () => { + const socialUserModel = createModel('SocialUser', [ + createField('id', 'String', {isId: true}), + createField('blocked', 'SocialUser', { + isList: true, + relationName: 'BlockList', + kind: 'object', + }), + createField('blockedBy', 'SocialUser', { + isList: true, + relationName: 'BlockList', + kind: 'object', + }), + ]); + + const dmmf = createMockDMMF([socialUserModel]); + const result = transformSchema(dmmf, baseConfig); + + const socialUser = result.models.find(m => m.modelName === 'SocialUser'); + expect(socialUser).toBeDefined(); + if (!socialUser) { + throw new Error('SocialUser model not found'); + } + + const blocked = socialUser.relationships.blocked; + const blockedBy = socialUser.relationships.blockedBy; + + if (!blocked || !blockedBy) { + throw new Error('Expected SocialUser to have both relationship fields'); + } + + if (!('chain' in blocked) || !('chain' in blockedBy)) { + throw new Error('Expected chained many-to-many relationships'); + } + + expect(blocked.chain[0]?.destField).toEqual(['A']); + expect(blocked.chain[1]?.sourceField).toEqual(['B']); + + expect(blockedBy.chain[0]?.destField).toEqual(['B']); + expect(blockedBy.chain[1]?.sourceField).toEqual(['A']); + }); }); it('should correctly map implicit many-to-many relationships with non-string primary keys', () => { From c938ea74123b0f1123173ba59e62d14baa1fba5e Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Thu, 11 Dec 2025 17:11:31 -0700 Subject: [PATCH 6/8] test: repro mapping issue --- integration/generated/zero/schema.ts | 82 ++++++++++------------------ integration/prisma.config.ts | 6 +- integration/schema.prisma | 2 +- 3 files changed, 30 insertions(+), 60 deletions(-) diff --git a/integration/generated/zero/schema.ts b/integration/generated/zero/schema.ts index 0e6113c..cb64514 100644 --- a/integration/generated/zero/schema.ts +++ b/integration/generated/zero/schema.ts @@ -17,8 +17,7 @@ export type Role = 'USER' | 'ADMIN'; export type Status = 'active' | 'inactive'; -export const scalarTypesTable = table('scalarTypes') - .from('ScalarTypes') +export const scalarTypesTable = table('ScalarTypes') .columns({ id: string(), str: string(), @@ -33,8 +32,7 @@ export const scalarTypesTable = table('scalarTypes') }) .primaryKey('id'); -export const optionalTypesTable = table('optionalTypes') - .from('OptionalTypes') +export const optionalTypesTable = table('OptionalTypes') .columns({ id: string(), str: string().optional(), @@ -45,8 +43,7 @@ export const optionalTypesTable = table('optionalTypes') }) .primaryKey('id'); -export const arrayTypesTable = table('arrayTypes') - .from('ArrayTypes') +export const arrayTypesTable = table('ArrayTypes') .columns({ id: string(), strings: json(), @@ -57,8 +54,7 @@ export const arrayTypesTable = table('arrayTypes') }) .primaryKey('id'); -export const fieldMappingTable = table('fieldMapping') - .from('FieldMapping') +export const fieldMappingTable = table('FieldMapping') .columns({ id: string(), firstName: string().from('first_name'), @@ -66,24 +62,21 @@ export const fieldMappingTable = table('fieldMapping') }) .primaryKey('id'); -export const tableMappingTable = table('tableMappings') - .from('table_mappings') +export const tableMappingTable = table('table_mappings') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const combinedMappingTable = table('combinedMappings') - .from('combined_mappings') +export const combinedMappingTable = table('combined_mappings') .columns({ id: string(), createdAt: number().from('created_at'), }) .primaryKey('id'); -export const compositePkTable = table('compositePk') - .from('CompositePK') +export const compositePkTable = table('CompositePK') .columns({ tenantId: string(), recordId: string(), @@ -91,16 +84,14 @@ export const compositePkTable = table('compositePk') }) .primaryKey('tenantId', 'recordId'); -export const userTable = table('user') - .from('User') +export const userTable = table('User') .columns({ id: string(), email: string(), }) .primaryKey('id'); -export const profileTable = table('profile') - .from('Profile') +export const profileTable = table('Profile') .columns({ id: string(), bio: string().optional(), @@ -108,8 +99,7 @@ export const profileTable = table('profile') }) .primaryKey('id'); -export const postTable = table('post') - .from('Post') +export const postTable = table('Post') .columns({ id: string(), title: string(), @@ -117,8 +107,7 @@ export const postTable = table('post') }) .primaryKey('id'); -export const commentTable = table('comment') - .from('Comment') +export const commentTable = table('Comment') .columns({ id: string(), text: string(), @@ -126,40 +115,35 @@ export const commentTable = table('comment') }) .primaryKey('id'); -export const articleTable = table('article') - .from('Article') +export const articleTable = table('Article') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const tagTable = table('tag') - .from('Tag') +export const tagTable = table('Tag') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const workerTable = table('worker') - .from('Worker') +export const workerTable = table('Worker') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const skillTable = table('skill') - .from('Skill') +export const skillTable = table('Skill') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const workerSkillTable = table('workerSkill') - .from('WorkerSkill') +export const workerSkillTable = table('WorkerSkill') .columns({ id: string(), proficiency: number(), @@ -168,8 +152,7 @@ export const workerSkillTable = table('workerSkill') }) .primaryKey('id'); -export const categoryTable = table('category') - .from('Category') +export const categoryTable = table('Category') .columns({ id: string(), name: string(), @@ -177,24 +160,21 @@ export const categoryTable = table('category') }) .primaryKey('id'); -export const socialUserTable = table('socialUser') - .from('SocialUser') +export const socialUserTable = table('SocialUser') .columns({ id: string(), username: string(), }) .primaryKey('id'); -export const tenantTable = table('tenant') - .from('Tenant') +export const tenantTable = table('Tenant') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const tenantConfigTable = table('tenantConfig') - .from('TenantConfig') +export const tenantConfigTable = table('TenantConfig') .columns({ id: string(), settings: json(), @@ -202,8 +182,7 @@ export const tenantConfigTable = table('tenantConfig') }) .primaryKey('id'); -export const taskTable = table('task') - .from('Task') +export const taskTable = table('Task') .columns({ id: string(), title: string(), @@ -212,16 +191,14 @@ export const taskTable = table('task') }) .primaryKey('id'); -export const memberTable = table('member') - .from('Member') +export const memberTable = table('Member') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const enumFieldsTable = table('enumFields') - .from('EnumFields') +export const enumFieldsTable = table('EnumFields') .columns({ id: string(), role: enumeration(), @@ -229,8 +206,7 @@ export const enumFieldsTable = table('enumFields') }) .primaryKey('id'); -export const nativeTypesTable = table('nativeTypes') - .from('NativeTypes') +export const nativeTypesTable = table('NativeTypes') .columns({ id: string(), varchar: string(), @@ -242,15 +218,13 @@ export const nativeTypesTable = table('nativeTypes') }) .primaryKey('id'); -export const minimalModelTable = table('minimalModel') - .from('MinimalModel') +export const minimalModelTable = table('MinimalModel') .columns({ id: string(), }) .primaryKey('id'); -export const reservedWordsTable = table('reservedWords') - .from('ReservedWords') +export const reservedWordsTable = table('ReservedWords') .columns({ id: string(), select: string().from('select_field'), @@ -259,7 +233,7 @@ export const reservedWordsTable = table('reservedWords') }) .primaryKey('id'); -export const _articleToTagTable = table('_articleToTag') +export const _articleToTagTable = table('_ArticleToTag') .from('_ArticleToTag') .columns({ A: string(), @@ -267,7 +241,7 @@ export const _articleToTagTable = table('_articleToTag') }) .primaryKey('A', 'B'); -export const _blockListTable = table('_blockList') +export const _blockListTable = table('_BlockList') .from('_BlockList') .columns({ A: string(), diff --git a/integration/prisma.config.ts b/integration/prisma.config.ts index 6de6a55..58722c6 100644 --- a/integration/prisma.config.ts +++ b/integration/prisma.config.ts @@ -1,12 +1,8 @@ import {defineConfig} from 'prisma/config'; -const databaseUrl = - process.env.DATABASE_URL ?? - 'postgresql://postgres:postgres@localhost:5432/prisma-zero'; - export default defineConfig({ schema: './schema.prisma', datasource: { - url: databaseUrl, + url: 'postgresql://postgres:postgres@localhost:5432/prisma-zero', }, }); diff --git a/integration/schema.prisma b/integration/schema.prisma index c127c53..fc05166 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -6,7 +6,7 @@ generator zero { provider = "prisma-zero" output = "./generated/zero" prettier = true - camelCase = true + // camelCase = true excludeTables = ["ExcludedModel"] } From 378733aba5e8c3b5e672965834da42ac4b2031a1 Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Thu, 11 Dec 2025 17:13:29 -0700 Subject: [PATCH 7/8] fix: fixed mapping table names --- integration/generated/zero/schema.ts | 6 +++-- src/mappers/schema-mapper.ts | 15 ++++++----- tests/schema-mapper.test.ts | 37 +++++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/integration/generated/zero/schema.ts b/integration/generated/zero/schema.ts index cb64514..bf84057 100644 --- a/integration/generated/zero/schema.ts +++ b/integration/generated/zero/schema.ts @@ -62,14 +62,16 @@ export const fieldMappingTable = table('FieldMapping') }) .primaryKey('id'); -export const tableMappingTable = table('table_mappings') +export const tableMappingTable = table('TableMapping') + .from('table_mappings') .columns({ id: string(), name: string(), }) .primaryKey('id'); -export const combinedMappingTable = table('combined_mappings') +export const combinedMappingTable = table('CombinedMapping') + .from('combined_mappings') .columns({ id: string(), createdAt: number().from('created_at'), diff --git a/src/mappers/schema-mapper.ts b/src/mappers/schema-mapper.ts index 2787d09..b429b3f 100644 --- a/src/mappers/schema-mapper.ts +++ b/src/mappers/schema-mapper.ts @@ -271,14 +271,17 @@ function mapModel( throw new Error(`No primary key found for ${model.name}`); } - const tableName = getTableNameFromModel(model); - const camelCasedName = config?.camelCase ? toCamelCase(tableName) : tableName; - - const shouldRemap = config.camelCase && camelCasedName !== tableName; + // Use the Prisma model name (optionally camelCased) for the Zero table name. + // If the Prisma model is mapped to a different DB table (@@map) or camelCase + // changes the casing, capture the DB table name in originalTableName so we + // can emit `.from("")` in the generated schema. + const databaseTableName = getTableNameFromModel(model); + const tableName = getTableName(model.name, config); + const shouldRemap = tableName !== databaseTableName; return { - tableName: shouldRemap ? camelCasedName : tableName, - originalTableName: shouldRemap ? tableName : null, + tableName, + originalTableName: shouldRemap ? databaseTableName : null, modelName: model.name, zeroTableName: getZeroTableName(model.name), columns, diff --git a/tests/schema-mapper.test.ts b/tests/schema-mapper.test.ts index 9241d4e..d26a291 100644 --- a/tests/schema-mapper.test.ts +++ b/tests/schema-mapper.test.ts @@ -126,6 +126,20 @@ describe('Schema Mapper', () => { expect(result.models[0]?.originalTableName).toBeNull(); }); + it('keeps Prisma model names when @@map is used and camelCase is false', () => { + const model = createModel( + 'TableMapping', + [createField('id', 'String', {isId: true})], + {dbName: 'table_mappings'}, + ); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + expect(result.models[0]?.tableName).toBe('TableMapping'); + expect(result.models[0]?.originalTableName).toBe('table_mappings'); + }); + it('should remap table names to camel case when camelCase is true', () => { const model = createModel('UserProfile', [ createField('id', 'String', {isId: true}), @@ -142,6 +156,23 @@ describe('Schema Mapper', () => { expect(result.models[0]?.originalTableName).toBe('UserProfile'); }); + it('camelCases the Prisma model name but preserves @@map database name', () => { + const model = createModel( + 'TableMapping', + [createField('id', 'String', {isId: true})], + {dbName: 'table_mappings'}, + ); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, { + ...baseConfig, + camelCase: true, + }); + + expect(result.models[0]?.tableName).toBe('tableMapping'); + expect(result.models[0]?.originalTableName).toBe('table_mappings'); + }); + it('should preserve table name if already in camel case', () => { const model = createModel('userProfile', [ createField('id', 'String', {isId: true}), @@ -176,7 +207,7 @@ describe('Schema Mapper', () => { camelCase: true, }); - expect(result.models[0]?.tableName).toBe('userProfile'); + expect(result.models[0]?.tableName).toBe('user'); expect(result.models[0]?.originalTableName).toBe('user_profile'); }); @@ -198,7 +229,7 @@ describe('Schema Mapper', () => { camelCase: true, }); - expect(result.models[0]?.tableName).toBe('userProfileSettings'); + expect(result.models[0]?.tableName).toBe('user'); expect(result.models[0]?.originalTableName).toBe('user_profile_settings'); }); @@ -220,7 +251,7 @@ describe('Schema Mapper', () => { camelCase: true, }); - expect(result.models[0]?.tableName).toBe('_userProfile'); + expect(result.models[0]?.tableName).toBe('userProfile'); expect(result.models[0]?.originalTableName).toBe('_UserProfile'); }); From 5654a314712892cb00974fedc3be7144c098a7bf Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Thu, 11 Dec 2025 17:20:48 -0700 Subject: [PATCH 8/8] chore: format --- tests/schema-mapper.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/schema-mapper.test.ts b/tests/schema-mapper.test.ts index d26a291..08eb7ed 100644 --- a/tests/schema-mapper.test.ts +++ b/tests/schema-mapper.test.ts @@ -104,10 +104,10 @@ describe('Schema Mapper', () => { const userModel = result.models.find(m => m.tableName === 'User'); expect(userModel).toBeDefined(); if (userModel) { - // Verify that the posts relationship field is not included - expect(userModel.relationships).not.toHaveProperty('posts'); - // Verify that the profile relationship field is still included - expect(userModel.relationships).toHaveProperty('profile'); + // Verify that the posts relationship field is not included + expect(userModel.relationships).not.toHaveProperty('posts'); + // Verify that the profile relationship field is still included + expect(userModel.relationships).toHaveProperty('profile'); } }); });