diff --git a/backend/migrations/tenant/20250305000001_create_contacts_and_contact_details.js b/backend/migrations/tenant/20250305000001_create_contacts_and_contact_details.js new file mode 100644 index 0000000..1ce78be --- /dev/null +++ b/backend/migrations/tenant/20250305000001_create_contacts_and_contact_details.js @@ -0,0 +1,197 @@ +exports.up = async function (knex) { + await knex.schema.createTable('contacts', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('(UUID())')); + table.string('firstName', 100).notNullable(); + table.string('lastName', 100).notNullable(); + table.uuid('accountId').notNullable(); + table.timestamps(true, true); + + table + .foreign('accountId') + .references('id') + .inTable('accounts') + .onDelete('CASCADE'); + table.index(['accountId']); + table.index(['lastName', 'firstName']); + }); + + await knex.schema.createTable('contact_details', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('(UUID())')); + table.string('relatedObjectType', 100).notNullable(); + table.uuid('relatedObjectId').notNullable(); + table.string('detailType', 50).notNullable(); + table.string('label', 100); + table.text('value').notNullable(); + table.boolean('isPrimary').defaultTo(false); + table.timestamps(true, true); + + table.index(['relatedObjectType', 'relatedObjectId']); + table.index(['detailType']); + }); + + const [contactObjectId] = await knex('object_definitions').insert({ + id: knex.raw('(UUID())'), + apiName: 'Contact', + label: 'Contact', + pluralLabel: 'Contacts', + description: 'Standard Contact object', + isSystem: true, + isCustom: false, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }); + + const contactObjectDefId = + contactObjectId || + (await knex('object_definitions').where('apiName', 'Contact').first()).id; + + await knex('field_definitions').insert([ + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactObjectDefId, + apiName: 'firstName', + label: 'First Name', + type: 'String', + length: 100, + isRequired: true, + isSystem: true, + isCustom: false, + displayOrder: 1, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactObjectDefId, + apiName: 'lastName', + label: 'Last Name', + type: 'String', + length: 100, + isRequired: true, + isSystem: true, + isCustom: false, + displayOrder: 2, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactObjectDefId, + apiName: 'accountId', + label: 'Account', + type: 'Reference', + referenceObject: 'Account', + isRequired: true, + isSystem: true, + isCustom: false, + displayOrder: 3, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + ]); + + const [contactDetailObjectId] = await knex('object_definitions').insert({ + id: knex.raw('(UUID())'), + apiName: 'ContactDetail', + label: 'Contact Detail', + pluralLabel: 'Contact Details', + description: 'Polymorphic contact detail object', + isSystem: true, + isCustom: false, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }); + + const contactDetailObjectDefId = + contactDetailObjectId || + (await knex('object_definitions').where('apiName', 'ContactDetail').first()) + .id; + + await knex('field_definitions').insert([ + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactDetailObjectDefId, + apiName: 'relatedObjectType', + label: 'Related Object Type', + type: 'String', + length: 100, + isRequired: true, + isSystem: true, + isCustom: false, + displayOrder: 1, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactDetailObjectDefId, + apiName: 'relatedObjectId', + label: 'Related Object ID', + type: 'String', + length: 36, + isRequired: true, + isSystem: true, + isCustom: false, + displayOrder: 2, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactDetailObjectDefId, + apiName: 'detailType', + label: 'Detail Type', + type: 'String', + length: 50, + isRequired: true, + isSystem: true, + isCustom: false, + displayOrder: 3, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactDetailObjectDefId, + apiName: 'label', + label: 'Label', + type: 'String', + length: 100, + isSystem: true, + isCustom: false, + displayOrder: 4, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactDetailObjectDefId, + apiName: 'value', + label: 'Value', + type: 'Text', + isRequired: true, + isSystem: true, + isCustom: false, + displayOrder: 5, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + { + id: knex.raw('(UUID())'), + objectDefinitionId: contactDetailObjectDefId, + apiName: 'isPrimary', + label: 'Primary', + type: 'Boolean', + isSystem: true, + isCustom: false, + displayOrder: 6, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }, + ]); +}; + +exports.down = async function (knex) { + await knex.schema.dropTableIfExists('contact_details'); + await knex.schema.dropTableIfExists('contacts'); +}; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index cecfd56..4e53e58 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -145,12 +145,42 @@ model Account { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id]) + owner User @relation(fields: [ownerId], references: [id]) + contacts Contact[] @@index([ownerId]) @@map("accounts") } +model Contact { + id String @id @default(uuid()) + firstName String + lastName String + accountId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) + + @@index([accountId]) + @@map("contacts") +} + +model ContactDetail { + id String @id @default(uuid()) + relatedObjectType String + relatedObjectId String + detailType String + label String? + value String + isPrimary Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([relatedObjectType, relatedObjectId]) + @@map("contact_details") +} + // Application Builder model App { id String @id @default(uuid()) diff --git a/backend/src/models/contact-detail.model.ts b/backend/src/models/contact-detail.model.ts new file mode 100644 index 0000000..e57c70e --- /dev/null +++ b/backend/src/models/contact-detail.model.ts @@ -0,0 +1,13 @@ +import { BaseModel } from './base.model'; + +export class ContactDetail extends BaseModel { + static tableName = 'contact_details'; + + id!: string; + relatedObjectType!: string; + relatedObjectId!: string; + detailType!: string; + label?: string; + value!: string; + isPrimary!: boolean; +} diff --git a/backend/src/models/contact.model.ts b/backend/src/models/contact.model.ts new file mode 100644 index 0000000..285c4dc --- /dev/null +++ b/backend/src/models/contact.model.ts @@ -0,0 +1,21 @@ +import { BaseModel } from './base.model'; + +export class Contact extends BaseModel { + static tableName = 'contacts'; + + id!: string; + firstName!: string; + lastName!: string; + accountId!: string; + + static relationMappings = { + account: { + relation: BaseModel.BelongsToOneRelation, + modelClass: 'account.model', + join: { + from: 'contacts.accountId', + to: 'accounts.id', + }, + }, + }; +}