/** * Entity template — Suppa v2 declarative migration. * The class IS the migration: the platform converges every tenant's table * to this shape on each application start. Replace __markers__; delete * any decorator you don't need. Rules: references/entities.md. * * File name: __kebab-case-plural__.entity.ts (e.g. stage-workflows.entity.ts) */ import { Column, Entity, Enum, FieldTypeEnum, File, ManyToOne, MultiFile, SystemBaseEntity, } from '@suppa/sdk'; import { __TargetEntity__ } from './__target-entity__.entity'; // Fixed value set? Declare a string enum + optional per-value titles map. // A global name '__EntityName__.__field__' is how seeds and other entities // reference these values — declare one ONLY if something does (rule 17). export enum __Field__Enum { Active = 'active', Done = 'done', } export const __Field__Titles = { [__Field__Enum.Active]: { title: { en: 'Active', uk: 'Активний' } }, [__Field__Enum.Done]: { title: { en: 'Done', uk: 'Виконаний' } }, }; @Entity({ name: '__EntityName__', // = class name; table + createSeed() key title: { en: '__EntityName__', uk: '__Українська назва__' }, key: '__entity-stable-key__', // survives renames — rename stays a rename importKeyFields: ['__businessKeyField__'], // BLOCKING QUESTION — stable+unique business key representativeFieldName: '__businessKeyField__', // BLOCKING QUESTION // options: { comments: false, favorites: false, approvals: false, // timeTracking: false, browsingHistory: false, trackChangeHistory: false, // globalSearch: false, notificationMutes: false, reminders: false }, }) export class __EntityName__ extends SystemBaseEntity { // NEVER declare: id, createdAt, updatedAt, deletedAt, createdBy, removedBy // (SystemBaseEntity provides them). // Scalar column — type ALWAYS from FieldTypeEnum, never a raw string. @Column({ name: '__businessKeyField__', type: FieldTypeEnum.Text, nullable: false }) __businessKeyField__: string; // default is a SQL EXPRESSION STRING: 'false', '0', "'draft'" — never a JS value. @Column({ name: 'isActive', type: FieldTypeEnum.Boolean, default: 'false' }) isActive: boolean; @Column({ name: 'amount', type: FieldTypeEnum.Numeric, decimalPlaces: 2 }) amount: number; // Date-only column — there is no FieldTypeEnum.Date; it is Timestamp + subType. // CAVEAT: subType is fixed at creation — getting 'date' vs plain timestamp // wrong later means a NEW field, not a conversion. // @Column({ name: 'birthday', type: FieldTypeEnum.Timestamp, subType: 'date' }) // birthday: string; // Encrypted at rest — Text only. // CAVEAT: the platform forces unique/searchable/canFilter/canSort/canGroup to // false and clears maxLength/minLength/mask — keep anything users must search // in a separate plaintext column. // @Column({ name: 'payload', type: FieldTypeEnum.Text, subType: 'encrypted' }) // payload: string; // Relation — ALWAYS a thunk (() => Target), never the class itself. // No inverse passed — it's optional on @ManyToOne and omitted by default. // TYPE THE PROPERTY: @ManyToOne holds ONE row, so the type is the target // class itself. Drop the '?' only because of nullable: false; a nullable // relation is written `__target__?: __TargetEntity__`. @ManyToOne(() => __TargetEntity__, { nullable: false }) __target__: __TargetEntity__; // The to-many decorators hold an ARRAY of the target. @ManyToMany takes no // inverse; @OneToMany / @ManyToManyBackRef REQUIRE one, and it must select // the property on the target that holds the other side. // @ManyToMany(() => __TargetEntity__, { title: { en: 'Peers' } }) // __peers__: __TargetEntity__[]; // // @OneToMany(() => __TargetEntity__, (entity) => entity.owner, { title: { en: 'Children' } }) // __children__: __TargetEntity__[]; // // @ManyToManyBackRef(() => __TargetEntity__, (entity) => entity.__peers__, { title: { en: 'Back refs' } }) // __backRefs__: __TargetEntity__[]; // Fixed choices — @Enum/@MultiEnum, NEVER @Column({ type: 'enum' }). // LOCAL enum — the DEFAULT, and what most enum fields should be: no first // argument, and the decorator derives the name .. @Enum(__Field__Enum, __Field__Titles, { nullable: false }) __field__: __Field__Enum; // GLOBAL enum — pass the name FIRST, and ONLY when the value set is shared // across entities or referenced from a seed. NOTE: seed.template.ts writes // `{ name: '__EntityName__.__field__', value: … }`, which IS such a // reference — so if you keep that line in the seed, this field's enum is // global and the name belongs here. Delete the seed's enum line, or use the // declaration below; do not leave the pair half-and-half. // @Enum('__EntityName__.__field__', __Field__Enum, __Field__Titles, { nullable: false }) // __field__: __Field__Enum; // Field options: canGroup/canSort/showInTable/editFromTable/canFilter are // already true and readOnly/searchable already false. Write one ONLY to // depart from that — never restate a default on every field. // @Column({ name: 'notes', type: FieldTypeEnum.Text, searchable: true }) // notes: string; // Attachments — @File/@MultiFile, NEVER @Column({ type: 'file' }) or Bytea. @File({ title: { en: 'Cover', uk: 'Обкладинка' } }) cover: any; @MultiFile({ title: { en: 'Attachments', uk: 'Вкладення' } }) attachments: any[]; } /* Class-level index/check decorators go ABOVE the class — add `Check`/`Index` * to the @suppa/sdk import above when you uncomment these: @Index('idx___table____col__', ['__col__']) @Index('idx___table___alive', ['deadline'], { where: '"deletedAt" IS NULL' }) @Check('chk___table____rule__', ['amount'], '"amount" >= 0') */