joist-codegen 2.4.0-next.3 → 2.4.0-next.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/EntityDbMetadata.cjs +5 -3
- package/build/EntityDbMetadata.cjs.map +1 -1
- package/build/EntityDbMetadata.d.cts +2 -0
- package/build/EntityDbMetadata.d.cts.map +1 -1
- package/build/EntityDbMetadata.d.mts +2 -0
- package/build/EntityDbMetadata.d.mts.map +1 -1
- package/build/EntityDbMetadata.js +5 -3
- package/build/EntityDbMetadata.js.map +1 -1
- package/build/config.cjs +1 -1
- package/build/config.cjs.map +1 -1
- package/build/config.js +1 -1
- package/build/config.js.map +1 -1
- package/build/generateEntityCodegenFile.cjs +5 -3
- package/build/generateEntityCodegenFile.cjs.map +1 -1
- package/build/generateEntityCodegenFile.d.cts.map +1 -1
- package/build/generateEntityCodegenFile.d.mts.map +1 -1
- package/build/generateEntityCodegenFile.js +5 -3
- package/build/generateEntityCodegenFile.js.map +1 -1
- package/build/generateMetadataFile.cjs +2 -1
- package/build/generateMetadataFile.cjs.map +1 -1
- package/build/generateMetadataFile.d.cts.map +1 -1
- package/build/generateMetadataFile.d.mts.map +1 -1
- package/build/generateMetadataFile.js +2 -1
- package/build/generateMetadataFile.js.map +1 -1
- package/build/scanEntityFiles.cjs +6 -1
- package/build/scanEntityFiles.cjs.map +1 -1
- package/build/scanEntityFiles.d.cts +1 -1
- package/build/scanEntityFiles.d.cts.map +1 -1
- package/build/scanEntityFiles.d.mts +1 -1
- package/build/scanEntityFiles.d.mts.map +1 -1
- package/build/scanEntityFiles.js +6 -1
- package/build/scanEntityFiles.js.map +1 -1
- package/package.json +6 -6
- package/skills/joist-test-factories/SKILL.md +93 -6
|
@@ -111,7 +111,7 @@ function generateFields(config, dbMeta, dbMetadata, columnRef) {
|
|
|
111
111
|
}`;
|
|
112
112
|
});
|
|
113
113
|
dbMetadata.enums.forEach((field) => {
|
|
114
|
-
const { fieldName, enumDetailType, notNull, isArray, columnType, derived } = field;
|
|
114
|
+
const { fieldName, enumDetailType, enumType, notNull, isArray, columnType, derived } = field;
|
|
115
115
|
const serdeType = isArray ? EnumArrayFieldSerde : EnumFieldSerde;
|
|
116
116
|
const columnTypeWithArray = `${columnType}${isArray ? "[]" : ""}`;
|
|
117
117
|
fields[fieldName] = code`
|
|
@@ -122,6 +122,7 @@ function generateFields(config, dbMeta, dbMetadata, columnRef) {
|
|
|
122
122
|
required: ${notNull},
|
|
123
123
|
derived: ${!derived ? false : `"${derived}"`},
|
|
124
124
|
enumDetailType: ${enumDetailType},
|
|
125
|
+
enumType: ${enumType},
|
|
125
126
|
serde: new ${SimpleFieldSerde}("${fieldName}", ${columnRef(field, code`new ${serdeType}("${columnTypeWithArray}", ${enumDetailType})`, columnArgs(field, dbMetadata))}),
|
|
126
127
|
immutable: false,
|
|
127
128
|
${maybeDefault(field)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateMetadataFile.js","names":[],"sources":["../src/generateMetadataFile.ts"],"sourcesContent":["import { camelCase } from \"change-case\";\nimport { type Code, type Import, code, imp } from \"ts-poet\";\n\nimport { type Config } from \"./config.ts\";\nimport {\n type DatabaseColumnType,\n type DbMetadata,\n type EntityDbMetadata,\n type OneToManyField,\n type PrimitiveField,\n} from \"./EntityDbMetadata.ts\";\nimport {\n BigIntSerde,\n Column,\n ColumnDescriptors,\n CustomSerdeAdapter,\n DateSerde,\n DecimalToNumberSerde,\n EntityMetadata,\n EnumArrayFieldSerde,\n EnumFieldSerde,\n JsonSerde,\n KeySerde,\n PlainDateSerde,\n PlainDateTimeSerde,\n PlainTimeSerde,\n PolyComponent,\n PrimitiveSerde,\n SimpleFieldSerde,\n SuperstructSerde,\n ZodSerde,\n ZonedDateTimeSerde,\n polymorphicField,\n} from \"./symbols.ts\";\nimport { mapSimpleDbTypeToTypescriptType, q } from \"./utils.ts\";\n\nexport function generateMetadataFile(config: Config, dbMeta: DbMetadata, meta: EntityDbMetadata): Code {\n const { entity, createdAt, updatedAt, deletedAt } = meta;\n\n const fields = generateFields(config, dbMeta, meta, columnReference);\n\n Object.values(fields).forEach((code) => code.asOneline());\n\n const maybeBaseType = meta.baseClassName ? `\"${meta.baseClassName}\"` : undefined;\n // We want to put inheritanceType: sti/cti onto base classes as well\n const maybeInheritanceType = meta.inheritanceType ? `inheritanceType: \"${meta.inheritanceType}\",` : \"\";\n const maybeStiColumn = meta.stiDiscriminatorField ? `stiDiscriminatorField: \"${meta.stiDiscriminatorField}\",` : \"\";\n const maybeStiValue = meta.stiDiscriminatorValue ? `stiDiscriminatorValue: ${meta.stiDiscriminatorValue},` : \"\";\n const maybeCtiAbstract = meta.abstract ? `ctiAbstract: ${meta.abstract},` : \"\";\n // Force subtype `timestampFields` to be `undefined` to ensure all runtime code is reading from the baseMeta values.\n // Force subtype `timestampFields` to be `undefined` to ensure all runtime code is reading from the baseMeta values.\n const maybeTimestampConfig = meta.baseClassName\n ? code`undefined`\n : code`\n {\n createdAt: ${q(createdAt?.fieldName)},\n updatedAt: ${q(updatedAt?.fieldName)},\n deletedAt: ${q(deletedAt?.fieldName)},\n }\n `;\n const maybeInsertionOrder = meta.nonDeferredFkOrder !== 0 ? `nonDeferredFkOrder: ${meta.nonDeferredFkOrder},` : ``;\n const uniqueBy = getUniqueBy(config, meta);\n const maybeUniqueBy = uniqueBy.length > 0 ? code`uniqueBy: ${JSON.stringify(uniqueBy)},` : code``;\n\n return code`\n export const ${entity.metaName}: ${EntityMetadata}<${entity.name}> = {\n cstr: ${entity.typeForMetadataFile},\n type: \"${entity.name}\",\n baseType: ${maybeBaseType}, ${maybeInheritanceType} ${maybeStiColumn} ${maybeStiValue} ${maybeCtiAbstract}\n idType: \"${config.idType ?? \"tagged-string\"}\",\n idDbType: \"${meta.primaryKey.columnType}\",\n tagName: \"${meta.tagName}\",\n tableName: \"${meta.tableName}\",\n supportsEmExecute: ${!meta.inheritanceType && meta.supportsEmExecute === true},\n fields: ${fields},\n columns: ${meta.primaryKey.columnOwner.metaName}Columns,\n allFields: {},\n orderBy: ${q(config.entities[meta.name]?.orderBy)},\n timestampFields: ${maybeTimestampConfig},\n config: ${entity.configConst},\n factory: ${imp(`new${entity.name}@./entities.ts`)},\n baseTypes: [],\n subTypes: [], ${maybeInsertionOrder} ${maybeUniqueBy}\n };\n\n (${entity.typeForMetadataFile} as any).metadata = ${entity.metaName};\n `;\n}\n\n/** Returns configured identities plus conservative database-backed unique identities. */\nfunction getUniqueBy(config: Config, meta: EntityDbMetadata): string[][] {\n const uniqueBy = config.entities[meta.name]?.uniqueBy ?? [];\n const seen = new Set<string>();\n return [...uniqueBy, ...(meta.uniqueConstraints ?? [])].filter((fields) => {\n const key = fields.join(\"\\u0000\");\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n}\n\n/** Emits fields that bind the already-declared physical columns. */\nfunction generateFields(\n config: Config,\n dbMeta: DbMetadata,\n dbMetadata: EntityDbMetadata,\n columnRef: (column: Pick<PrimitiveField, \"columnName\" | \"columnOwner\">, codec?: Code, args?: Code) => Code,\n): Record<string, Code> {\n const fields: Record<string, Code> = {};\n\n fields[\"id\"] = code`\n {\n kind: \"primaryKey\",\n fieldName: \"id\",\n fieldIdName: undefined,\n required: true,\n serde: new ${SimpleFieldSerde}(\"id\", ${columnRef(dbMetadata.primaryKey, code`new ${KeySerde}(\"${dbMetadata.tagName}\", \"${dbMetadata.primaryKey.columnType}\")`, columnArgs(dbMetadata.primaryKey, dbMetadata, code`() => ${dbMetadata.entity.metaName}`))}),\n immutable: true,\n }\n `;\n\n dbMetadata.primitives.forEach((p) => {\n const { fieldName, derived, columnType } = p;\n const column = columnArgs(p, dbMetadata);\n const serde = primitiveColumnCodec(config, p);\n const extras = columnType === \"citext\" ? code`citext: true,` : \"\";\n fields[fieldName] = code`\n {\n kind: \"primitive\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n derived: ${!derived ? false : `\"${derived}\"`},\n required: ${!derived && p.notNull},\n protected: ${p.protected},\n type: ${typeof p.rawFieldType === \"string\" ? `\"${p.rawFieldType}\"` : p.rawFieldType},\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(p, serde, column)}),\n immutable: false,\n ${p.lazy ? code`lazy: true,` : \"\"}\n ${extras}\n ${maybeDefault(p)}\n ${maybeSanitize(config, p)}\n }`;\n });\n\n // Treat native enums as primitives\n dbMetadata.pgEnums.forEach((p) => {\n const { fieldName, notNull, dbType } = p;\n fields[fieldName] = code`\n {\n kind: \"primitive\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n derived: false,\n required: ${notNull},\n protected: false,\n type: \"string\",\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(p, code`new ${PrimitiveSerde}(\"${dbType}\")`, columnArgs(p, dbMetadata))}),\n immutable: false,\n ${maybeDefault(p)}\n }`;\n });\n\n dbMetadata.enums.forEach((field) => {\n const { fieldName, enumDetailType, notNull, isArray, columnType, derived } = field;\n const serdeType = isArray ? EnumArrayFieldSerde : EnumFieldSerde;\n const columnTypeWithArray = `${columnType}${isArray ? \"[]\" : \"\"}`;\n fields[fieldName] = code`\n {\n kind: \"enum\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n required: ${notNull},\n derived: ${!derived ? false : `\"${derived}\"`},\n enumDetailType: ${enumDetailType},\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(field, code`new ${serdeType}(\"${columnTypeWithArray}\", ${enumDetailType})`, columnArgs(field, dbMetadata))}),\n immutable: false,\n ${maybeDefault(field)}\n }\n `;\n });\n\n dbMetadata.manyToOnes.forEach((m2o) => {\n const { fieldName, columnName, notNull, otherEntity, otherFieldName, derived, dbType } = m2o;\n const otherTagName = config.entities[otherEntity.name].tag;\n const physical = (dbMetadata.physicalMetadata ?? dbMetadata).manyToOnes.find(\n (field) => field.columnName === columnName,\n );\n const otherMetadata =\n physical?.otherEntity.name === otherEntity.name\n ? code`${columnRef(m2o)}.idMetadata!`\n : code`() => ${otherEntity.metaName}`;\n fields[fieldName] = code`\n {\n kind: \"m2o\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${fieldName}Id\",\n derived: ${!derived ? false : `\"${derived}\"`},\n required: ${notNull},\n otherMetadata: ${otherMetadata},\n otherFieldName: \"${otherFieldName}\",\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(m2o, code`new ${KeySerde}(\"${otherTagName}\", \"${dbType}\")`, columnArgs(m2o, dbMetadata, code`() => ${otherEntity.metaName}`))}),\n immutable: false,\n ${maybeDefault(m2o)}\n }\n `;\n });\n\n dbMetadata.oneToManys.forEach((o2m) => {\n const { fieldName, singularName, otherEntity, otherFieldName, otherColumnName } = o2m;\n fields[fieldName] = code`\n {\n kind: \"o2m\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${singularName}Ids\",\n required: false,\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n otherColumnName: \"${otherColumnName}\",\n serde: undefined,\n immutable: false,\n ${maybeOrderBy(o2m)}\n ${maybeSoftDeletes(o2m)}\n }\n `;\n });\n\n dbMetadata.largeOneToManys.forEach((o2m) => {\n const { fieldName, singularName, otherEntity, otherFieldName, otherColumnName } = o2m;\n fields[fieldName] = code`\n {\n kind: \"lo2m\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${singularName}Ids\",\n required: false,\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n otherColumnName: \"${otherColumnName}\",\n serde: undefined,\n immutable: false,\n }\n `;\n });\n\n dbMetadata.manyToManys.forEach((m2m) => {\n const { fieldName, singularName, otherEntity, otherFieldName, derived } = m2m;\n fields[fieldName] = code`\n {\n kind: \"m2m\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${singularName}Ids\",\n required: false,\n derived: ${derived === \"async\" ? `\"async\"` : derived === \"otherSide\" ? `\"otherSide\"` : \"false\"},\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n serde: undefined,\n immutable: false,\n joinTableName: \"${m2m.joinTableName}\",\n columnNames: [\"${m2m.columnName}\", \"${m2m.otherColumnName}\"],\n hasJoinTableId: ${m2m.hasJoinTableId},\n ${maybeSoftDeletes(m2m)}\n }\n `;\n });\n\n dbMetadata.manyToManyEnums.forEach((m2m) => {\n const { fieldName, enumDetailType } = m2m;\n fields[fieldName] = code`\n {\n kind: \"m2mEnum\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n required: false,\n derived: false,\n enumDetailType: ${enumDetailType},\n serde: undefined,\n immutable: false,\n joinTableName: \"${m2m.joinTableName}\",\n columnNames: [\"${m2m.columnName}\", \"${m2m.otherColumnName}\"],\n hasJoinTableId: ${m2m.hasJoinTableId},\n }\n `;\n });\n\n dbMetadata.oneToOnes.forEach((o2o) => {\n const { fieldName, otherEntity, otherFieldName, otherColumnName } = o2o;\n fields[fieldName] = code`\n {\n kind: \"o2o\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${fieldName}Id\",\n required: false,\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n otherColumnName: \"${otherColumnName}\",\n serde: undefined,\n immutable: false,\n }\n `;\n });\n\n dbMetadata.polymorphics.forEach((p) => {\n const { fieldName, notNull, components } = p;\n components.forEach((component) =>\n columnRef(\n component,\n code`new ${KeySerde}(\"${config.entities[component.otherEntity.name].tag}\", \"${dbMeta.entitiesByName[component.otherEntity.name].primaryKey.columnType}\")`,\n code`true, false, false, false, false, () => ${component.otherEntity.metaName}`,\n ),\n );\n fields[fieldName] = code`\n ${polymorphicField}(\"${fieldName}\", ${notNull}, [${components.map((component) => {\n const owner = dbMeta.entitiesByName[component.columnOwner.name];\n const physical = (owner.physicalMetadata ?? owner).polymorphics\n .flatMap((field) => field.components)\n .find((c) => c.columnName === component.columnName)!;\n const target =\n physical.otherEntity.name === component.otherEntity.name\n ? \"\"\n : code`, () => ${component.otherEntity.metaName}`;\n return code`new ${PolyComponent}(${columnRef(component)}, ${q(component.otherFieldName)}${target}),`;\n })}])\n `;\n });\n\n return fields;\n}\n\n/** Emits constructor arguments using codegen's existing column facts. */\nfunction columnArgs(\n column: Pick<PrimitiveField, \"columnNotNull\" | \"columnGenerated\"> & Partial<Pick<PrimitiveField, \"columnDefault\">>,\n meta: EntityDbMetadata,\n idMetadata?: Code,\n): Code {\n const insertOptional = column === meta.createdAt || column === meta.updatedAt;\n return code`${!column.columnNotNull}, ${column.columnDefault != null && !column.columnGenerated}, ${column.columnGenerated}, ${insertOptional}, true, ${idMetadata ?? \"undefined\"}`;\n}\n\n/** Declares all physical codecs by their SQL API names before any domain metadata; ID targets remain lazy. */\nexport function generateColumnDeclarations(config: Config, dbMeta: DbMetadata, meta: EntityDbMetadata): Code {\n if (meta.inheritanceType === \"sti\" && meta.baseClassName) return code``;\n const columns: Record<string, Code> = {};\n generateFields(config, dbMeta, meta.physicalMetadata ?? meta, (column, codec, args) => {\n const columnName = column.columnName;\n const apiName = camelCase(columnName);\n if (codec) columns[apiName] = code`new ${Column}(${q(columnName)}, ${args}, ${codec})`.asOneline();\n return code`${meta.entity.metaName}Columns[${q(apiName)}]`;\n });\n for (const column of (meta.physicalMetadata ?? meta).ignoredColumns) {\n const apiName = camelCase(column.columnName);\n columns[apiName] =\n code`new ${Column}(${q(column.columnName)}, ${columnArgs(column, meta)}, ${primitiveColumnCodec(config, column)})`.asOneline();\n }\n return code`const ${meta.entity.metaName}Columns = ${columns} satisfies ${ColumnDescriptors};`;\n}\n\n/** Emits a direct reference to a descriptor by its SQL API name. */\nfunction columnReference(column: Pick<PrimitiveField, \"columnName\" | \"columnOwner\">): Code {\n return code`${column.columnOwner.metaName}Columns[${q(camelCase(column.columnName))}]`;\n}\n\nfunction maybeDefault(f: { hasConfigDefault: boolean; columnDefault?: number | boolean | string | null }): Code | \"\" {\n return f.hasConfigDefault ? code`default: \"config\",` : f.columnDefault != null ? code`default: \"schema\",` : \"\";\n}\n\nfunction maybeOrderBy(f: OneToManyField): Code | \"\" {\n return f.orderBy ? code`orderBy: { field: \"${f.orderBy.field}\", direction: \"${f.orderBy.direction}\" },` : \"\";\n}\n\n/** Emits the `softDeletes` config, i.e. `softDeletes: \"include\",`, for relations that opt out of hiding soft-deletes. */\nfunction maybeSoftDeletes(f: { softDeletes: \"include\" | \"exclude\" | undefined }): Code | \"\" {\n return f.softDeletes ? code`softDeletes: \"${f.softDeletes}\",` : \"\";\n}\n\n/** We sanitize/cleanStringValue all varchars, unless opted out by the column default/arrays/custom serdes. */\nfunction maybeSanitize(\n config: Config,\n f: { columnType: DatabaseColumnType; columnDefault?: any; isArray: boolean; customSerde: any },\n): Code | \"\" {\n // Only strings need to maybe turn off their cleanStringValue sanitization, if the default=\"\" or its an array\n return isString(config, f.columnType) && (f.columnDefault === \"''\" || f.isArray || f.customSerde)\n ? code`sanitize: false,`\n : \"\";\n}\n\nfunction isString(config: Config, columnType: DatabaseColumnType): boolean {\n return mapSimpleDbTypeToTypescriptType(config, columnType) === \"string\";\n}\n\n/** Uses domain codecs for modeled fields and the same native conversions for unmodeled columns. */\nfunction primitiveColumnCodec(config: Config, column: PrimitiveField): Code {\n const { columnType, customSerde, superstruct, zodSchema, isArray } = column;\n if (customSerde) {\n return isArray\n ? code`new ${CustomSerdeAdapter}( \"${columnType}[]\", ${customSerde}, true)`\n : code`new ${CustomSerdeAdapter}( \"${columnType}\", ${customSerde})`;\n }\n if (superstruct) return code`new ${SuperstructSerde}( ${superstruct})`;\n if (zodSchema) return code`new ${ZodSerde}( ${zodSchema})`;\n if (columnType === \"numeric\" || columnType === \"decimal\") {\n return isArray ? code`new ${DecimalToNumberSerde}(true)` : code`new ${DecimalToNumberSerde}()`;\n }\n if (columnType === \"jsonb\") return code`new ${JsonSerde}()`;\n if (column.rawFieldType === \"bigint\") {\n return isArray ? code`new ${BigIntSerde}(true)` : code`new ${BigIntSerde}()`;\n }\n\n let serdeType: Import;\n if (columnType === \"date\") {\n serdeType = config.temporal ? PlainDateSerde : DateSerde;\n } else if (columnType === \"timestamp without time zone\") {\n serdeType = config.temporal ? PlainDateTimeSerde : DateSerde;\n } else if (columnType === \"timestamp with time zone\") {\n serdeType = config.temporal ? ZonedDateTimeSerde : DateSerde;\n } else if (columnType === \"time without time zone\") {\n serdeType = config.temporal ? PlainTimeSerde : PrimitiveSerde;\n } else {\n serdeType = PrimitiveSerde;\n }\n return isArray ? code`new ${serdeType}( \"${columnType}[]\", true)` : code`new ${serdeType}( \"${columnType}\")`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generateMetadataFile.js","names":[],"sources":["../src/generateMetadataFile.ts"],"sourcesContent":["import { camelCase } from \"change-case\";\nimport { type Code, type Import, code, imp } from \"ts-poet\";\n\nimport { type Config } from \"./config.ts\";\nimport {\n type DatabaseColumnType,\n type DbMetadata,\n type EntityDbMetadata,\n type OneToManyField,\n type PrimitiveField,\n} from \"./EntityDbMetadata.ts\";\nimport {\n BigIntSerde,\n Column,\n ColumnDescriptors,\n CustomSerdeAdapter,\n DateSerde,\n DecimalToNumberSerde,\n EntityMetadata,\n EnumArrayFieldSerde,\n EnumFieldSerde,\n JsonSerde,\n KeySerde,\n PlainDateSerde,\n PlainDateTimeSerde,\n PlainTimeSerde,\n PolyComponent,\n PrimitiveSerde,\n SimpleFieldSerde,\n SuperstructSerde,\n ZodSerde,\n ZonedDateTimeSerde,\n polymorphicField,\n} from \"./symbols.ts\";\nimport { mapSimpleDbTypeToTypescriptType, q } from \"./utils.ts\";\n\nexport function generateMetadataFile(config: Config, dbMeta: DbMetadata, meta: EntityDbMetadata): Code {\n const { entity, createdAt, updatedAt, deletedAt } = meta;\n\n const fields = generateFields(config, dbMeta, meta, columnReference);\n\n Object.values(fields).forEach((code) => code.asOneline());\n\n const maybeBaseType = meta.baseClassName ? `\"${meta.baseClassName}\"` : undefined;\n // We want to put inheritanceType: sti/cti onto base classes as well\n const maybeInheritanceType = meta.inheritanceType ? `inheritanceType: \"${meta.inheritanceType}\",` : \"\";\n const maybeStiColumn = meta.stiDiscriminatorField ? `stiDiscriminatorField: \"${meta.stiDiscriminatorField}\",` : \"\";\n const maybeStiValue = meta.stiDiscriminatorValue ? `stiDiscriminatorValue: ${meta.stiDiscriminatorValue},` : \"\";\n const maybeCtiAbstract = meta.abstract ? `ctiAbstract: ${meta.abstract},` : \"\";\n // Force subtype `timestampFields` to be `undefined` to ensure all runtime code is reading from the baseMeta values.\n // Force subtype `timestampFields` to be `undefined` to ensure all runtime code is reading from the baseMeta values.\n const maybeTimestampConfig = meta.baseClassName\n ? code`undefined`\n : code`\n {\n createdAt: ${q(createdAt?.fieldName)},\n updatedAt: ${q(updatedAt?.fieldName)},\n deletedAt: ${q(deletedAt?.fieldName)},\n }\n `;\n const maybeInsertionOrder = meta.nonDeferredFkOrder !== 0 ? `nonDeferredFkOrder: ${meta.nonDeferredFkOrder},` : ``;\n const uniqueBy = getUniqueBy(config, meta);\n const maybeUniqueBy = uniqueBy.length > 0 ? code`uniqueBy: ${JSON.stringify(uniqueBy)},` : code``;\n\n return code`\n export const ${entity.metaName}: ${EntityMetadata}<${entity.name}> = {\n cstr: ${entity.typeForMetadataFile},\n type: \"${entity.name}\",\n baseType: ${maybeBaseType}, ${maybeInheritanceType} ${maybeStiColumn} ${maybeStiValue} ${maybeCtiAbstract}\n idType: \"${config.idType ?? \"tagged-string\"}\",\n idDbType: \"${meta.primaryKey.columnType}\",\n tagName: \"${meta.tagName}\",\n tableName: \"${meta.tableName}\",\n supportsEmExecute: ${!meta.inheritanceType && meta.supportsEmExecute === true},\n fields: ${fields},\n columns: ${meta.primaryKey.columnOwner.metaName}Columns,\n allFields: {},\n orderBy: ${q(config.entities[meta.name]?.orderBy)},\n timestampFields: ${maybeTimestampConfig},\n config: ${entity.configConst},\n factory: ${imp(`new${entity.name}@./entities.ts`)},\n baseTypes: [],\n subTypes: [], ${maybeInsertionOrder} ${maybeUniqueBy}\n };\n\n (${entity.typeForMetadataFile} as any).metadata = ${entity.metaName};\n `;\n}\n\n/** Returns configured identities plus conservative database-backed unique identities. */\nfunction getUniqueBy(config: Config, meta: EntityDbMetadata): string[][] {\n const uniqueBy = config.entities[meta.name]?.uniqueBy ?? [];\n const seen = new Set<string>();\n return [...uniqueBy, ...(meta.uniqueConstraints ?? [])].filter((fields) => {\n const key = fields.join(\"\\u0000\");\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n}\n\n/** Emits fields that bind the already-declared physical columns. */\nfunction generateFields(\n config: Config,\n dbMeta: DbMetadata,\n dbMetadata: EntityDbMetadata,\n columnRef: (column: Pick<PrimitiveField, \"columnName\" | \"columnOwner\">, codec?: Code, args?: Code) => Code,\n): Record<string, Code> {\n const fields: Record<string, Code> = {};\n\n fields[\"id\"] = code`\n {\n kind: \"primaryKey\",\n fieldName: \"id\",\n fieldIdName: undefined,\n required: true,\n serde: new ${SimpleFieldSerde}(\"id\", ${columnRef(dbMetadata.primaryKey, code`new ${KeySerde}(\"${dbMetadata.tagName}\", \"${dbMetadata.primaryKey.columnType}\")`, columnArgs(dbMetadata.primaryKey, dbMetadata, code`() => ${dbMetadata.entity.metaName}`))}),\n immutable: true,\n }\n `;\n\n dbMetadata.primitives.forEach((p) => {\n const { fieldName, derived, columnType } = p;\n const column = columnArgs(p, dbMetadata);\n const serde = primitiveColumnCodec(config, p);\n const extras = columnType === \"citext\" ? code`citext: true,` : \"\";\n fields[fieldName] = code`\n {\n kind: \"primitive\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n derived: ${!derived ? false : `\"${derived}\"`},\n required: ${!derived && p.notNull},\n protected: ${p.protected},\n type: ${typeof p.rawFieldType === \"string\" ? `\"${p.rawFieldType}\"` : p.rawFieldType},\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(p, serde, column)}),\n immutable: false,\n ${p.lazy ? code`lazy: true,` : \"\"}\n ${extras}\n ${maybeDefault(p)}\n ${maybeSanitize(config, p)}\n }`;\n });\n\n // Treat native enums as primitives\n dbMetadata.pgEnums.forEach((p) => {\n const { fieldName, notNull, dbType } = p;\n fields[fieldName] = code`\n {\n kind: \"primitive\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n derived: false,\n required: ${notNull},\n protected: false,\n type: \"string\",\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(p, code`new ${PrimitiveSerde}(\"${dbType}\")`, columnArgs(p, dbMetadata))}),\n immutable: false,\n ${maybeDefault(p)}\n }`;\n });\n\n dbMetadata.enums.forEach((field) => {\n const { fieldName, enumDetailType, enumType, notNull, isArray, columnType, derived } = field;\n const serdeType = isArray ? EnumArrayFieldSerde : EnumFieldSerde;\n const columnTypeWithArray = `${columnType}${isArray ? \"[]\" : \"\"}`;\n fields[fieldName] = code`\n {\n kind: \"enum\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n required: ${notNull},\n derived: ${!derived ? false : `\"${derived}\"`},\n enumDetailType: ${enumDetailType},\n enumType: ${enumType},\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(field, code`new ${serdeType}(\"${columnTypeWithArray}\", ${enumDetailType})`, columnArgs(field, dbMetadata))}),\n immutable: false,\n ${maybeDefault(field)}\n }\n `;\n });\n\n dbMetadata.manyToOnes.forEach((m2o) => {\n const { fieldName, columnName, notNull, otherEntity, otherFieldName, derived, dbType } = m2o;\n const otherTagName = config.entities[otherEntity.name].tag;\n const physical = (dbMetadata.physicalMetadata ?? dbMetadata).manyToOnes.find(\n (field) => field.columnName === columnName,\n );\n const otherMetadata =\n physical?.otherEntity.name === otherEntity.name\n ? code`${columnRef(m2o)}.idMetadata!`\n : code`() => ${otherEntity.metaName}`;\n fields[fieldName] = code`\n {\n kind: \"m2o\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${fieldName}Id\",\n derived: ${!derived ? false : `\"${derived}\"`},\n required: ${notNull},\n otherMetadata: ${otherMetadata},\n otherFieldName: \"${otherFieldName}\",\n serde: new ${SimpleFieldSerde}(\"${fieldName}\", ${columnRef(m2o, code`new ${KeySerde}(\"${otherTagName}\", \"${dbType}\")`, columnArgs(m2o, dbMetadata, code`() => ${otherEntity.metaName}`))}),\n immutable: false,\n ${maybeDefault(m2o)}\n }\n `;\n });\n\n dbMetadata.oneToManys.forEach((o2m) => {\n const { fieldName, singularName, otherEntity, otherFieldName, otherColumnName } = o2m;\n fields[fieldName] = code`\n {\n kind: \"o2m\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${singularName}Ids\",\n required: false,\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n otherColumnName: \"${otherColumnName}\",\n serde: undefined,\n immutable: false,\n ${maybeOrderBy(o2m)}\n ${maybeSoftDeletes(o2m)}\n }\n `;\n });\n\n dbMetadata.largeOneToManys.forEach((o2m) => {\n const { fieldName, singularName, otherEntity, otherFieldName, otherColumnName } = o2m;\n fields[fieldName] = code`\n {\n kind: \"lo2m\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${singularName}Ids\",\n required: false,\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n otherColumnName: \"${otherColumnName}\",\n serde: undefined,\n immutable: false,\n }\n `;\n });\n\n dbMetadata.manyToManys.forEach((m2m) => {\n const { fieldName, singularName, otherEntity, otherFieldName, derived } = m2m;\n fields[fieldName] = code`\n {\n kind: \"m2m\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${singularName}Ids\",\n required: false,\n derived: ${derived === \"async\" ? `\"async\"` : derived === \"otherSide\" ? `\"otherSide\"` : \"false\"},\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n serde: undefined,\n immutable: false,\n joinTableName: \"${m2m.joinTableName}\",\n columnNames: [\"${m2m.columnName}\", \"${m2m.otherColumnName}\"],\n hasJoinTableId: ${m2m.hasJoinTableId},\n ${maybeSoftDeletes(m2m)}\n }\n `;\n });\n\n dbMetadata.manyToManyEnums.forEach((m2m) => {\n const { fieldName, enumDetailType } = m2m;\n fields[fieldName] = code`\n {\n kind: \"m2mEnum\",\n fieldName: \"${fieldName}\",\n fieldIdName: undefined,\n required: false,\n derived: false,\n enumDetailType: ${enumDetailType},\n serde: undefined,\n immutable: false,\n joinTableName: \"${m2m.joinTableName}\",\n columnNames: [\"${m2m.columnName}\", \"${m2m.otherColumnName}\"],\n hasJoinTableId: ${m2m.hasJoinTableId},\n }\n `;\n });\n\n dbMetadata.oneToOnes.forEach((o2o) => {\n const { fieldName, otherEntity, otherFieldName, otherColumnName } = o2o;\n fields[fieldName] = code`\n {\n kind: \"o2o\",\n fieldName: \"${fieldName}\",\n fieldIdName: \"${fieldName}Id\",\n required: false,\n otherMetadata: () => ${otherEntity.metaName},\n otherFieldName: \"${otherFieldName}\",\n otherColumnName: \"${otherColumnName}\",\n serde: undefined,\n immutable: false,\n }\n `;\n });\n\n dbMetadata.polymorphics.forEach((p) => {\n const { fieldName, notNull, components } = p;\n components.forEach((component) =>\n columnRef(\n component,\n code`new ${KeySerde}(\"${config.entities[component.otherEntity.name].tag}\", \"${dbMeta.entitiesByName[component.otherEntity.name].primaryKey.columnType}\")`,\n code`true, false, false, false, false, () => ${component.otherEntity.metaName}`,\n ),\n );\n fields[fieldName] = code`\n ${polymorphicField}(\"${fieldName}\", ${notNull}, [${components.map((component) => {\n const owner = dbMeta.entitiesByName[component.columnOwner.name];\n const physical = (owner.physicalMetadata ?? owner).polymorphics\n .flatMap((field) => field.components)\n .find((c) => c.columnName === component.columnName)!;\n const target =\n physical.otherEntity.name === component.otherEntity.name\n ? \"\"\n : code`, () => ${component.otherEntity.metaName}`;\n return code`new ${PolyComponent}(${columnRef(component)}, ${q(component.otherFieldName)}${target}),`;\n })}])\n `;\n });\n\n return fields;\n}\n\n/** Emits constructor arguments using codegen's existing column facts. */\nfunction columnArgs(\n column: Pick<PrimitiveField, \"columnNotNull\" | \"columnGenerated\"> & Partial<Pick<PrimitiveField, \"columnDefault\">>,\n meta: EntityDbMetadata,\n idMetadata?: Code,\n): Code {\n const insertOptional = column === meta.createdAt || column === meta.updatedAt;\n return code`${!column.columnNotNull}, ${column.columnDefault != null && !column.columnGenerated}, ${column.columnGenerated}, ${insertOptional}, true, ${idMetadata ?? \"undefined\"}`;\n}\n\n/** Declares all physical codecs by their SQL API names before any domain metadata; ID targets remain lazy. */\nexport function generateColumnDeclarations(config: Config, dbMeta: DbMetadata, meta: EntityDbMetadata): Code {\n if (meta.inheritanceType === \"sti\" && meta.baseClassName) return code``;\n const columns: Record<string, Code> = {};\n generateFields(config, dbMeta, meta.physicalMetadata ?? meta, (column, codec, args) => {\n const columnName = column.columnName;\n const apiName = camelCase(columnName);\n if (codec) columns[apiName] = code`new ${Column}(${q(columnName)}, ${args}, ${codec})`.asOneline();\n return code`${meta.entity.metaName}Columns[${q(apiName)}]`;\n });\n for (const column of (meta.physicalMetadata ?? meta).ignoredColumns) {\n const apiName = camelCase(column.columnName);\n columns[apiName] =\n code`new ${Column}(${q(column.columnName)}, ${columnArgs(column, meta)}, ${primitiveColumnCodec(config, column)})`.asOneline();\n }\n return code`const ${meta.entity.metaName}Columns = ${columns} satisfies ${ColumnDescriptors};`;\n}\n\n/** Emits a direct reference to a descriptor by its SQL API name. */\nfunction columnReference(column: Pick<PrimitiveField, \"columnName\" | \"columnOwner\">): Code {\n return code`${column.columnOwner.metaName}Columns[${q(camelCase(column.columnName))}]`;\n}\n\nfunction maybeDefault(f: { hasConfigDefault: boolean; columnDefault?: number | boolean | string | null }): Code | \"\" {\n return f.hasConfigDefault ? code`default: \"config\",` : f.columnDefault != null ? code`default: \"schema\",` : \"\";\n}\n\nfunction maybeOrderBy(f: OneToManyField): Code | \"\" {\n return f.orderBy ? code`orderBy: { field: \"${f.orderBy.field}\", direction: \"${f.orderBy.direction}\" },` : \"\";\n}\n\n/** Emits the `softDeletes` config, i.e. `softDeletes: \"include\",`, for relations that opt out of hiding soft-deletes. */\nfunction maybeSoftDeletes(f: { softDeletes: \"include\" | \"exclude\" | undefined }): Code | \"\" {\n return f.softDeletes ? code`softDeletes: \"${f.softDeletes}\",` : \"\";\n}\n\n/** We sanitize/cleanStringValue all varchars, unless opted out by the column default/arrays/custom serdes. */\nfunction maybeSanitize(\n config: Config,\n f: { columnType: DatabaseColumnType; columnDefault?: any; isArray: boolean; customSerde: any },\n): Code | \"\" {\n // Only strings need to maybe turn off their cleanStringValue sanitization, if the default=\"\" or its an array\n return isString(config, f.columnType) && (f.columnDefault === \"''\" || f.isArray || f.customSerde)\n ? code`sanitize: false,`\n : \"\";\n}\n\nfunction isString(config: Config, columnType: DatabaseColumnType): boolean {\n return mapSimpleDbTypeToTypescriptType(config, columnType) === \"string\";\n}\n\n/** Uses domain codecs for modeled fields and the same native conversions for unmodeled columns. */\nfunction primitiveColumnCodec(config: Config, column: PrimitiveField): Code {\n const { columnType, customSerde, superstruct, zodSchema, isArray } = column;\n if (customSerde) {\n return isArray\n ? code`new ${CustomSerdeAdapter}( \"${columnType}[]\", ${customSerde}, true)`\n : code`new ${CustomSerdeAdapter}( \"${columnType}\", ${customSerde})`;\n }\n if (superstruct) return code`new ${SuperstructSerde}( ${superstruct})`;\n if (zodSchema) return code`new ${ZodSerde}( ${zodSchema})`;\n if (columnType === \"numeric\" || columnType === \"decimal\") {\n return isArray ? code`new ${DecimalToNumberSerde}(true)` : code`new ${DecimalToNumberSerde}()`;\n }\n if (columnType === \"jsonb\") return code`new ${JsonSerde}()`;\n if (column.rawFieldType === \"bigint\") {\n return isArray ? code`new ${BigIntSerde}(true)` : code`new ${BigIntSerde}()`;\n }\n\n let serdeType: Import;\n if (columnType === \"date\") {\n serdeType = config.temporal ? PlainDateSerde : DateSerde;\n } else if (columnType === \"timestamp without time zone\") {\n serdeType = config.temporal ? PlainDateTimeSerde : DateSerde;\n } else if (columnType === \"timestamp with time zone\") {\n serdeType = config.temporal ? ZonedDateTimeSerde : DateSerde;\n } else if (columnType === \"time without time zone\") {\n serdeType = config.temporal ? PlainTimeSerde : PrimitiveSerde;\n } else {\n serdeType = PrimitiveSerde;\n }\n return isArray ? code`new ${serdeType}( \"${columnType}[]\", true)` : code`new ${serdeType}( \"${columnType}\")`;\n}\n"],"mappings":"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"}
|
|
@@ -5,7 +5,8 @@ let path = require("path");
|
|
|
5
5
|
let fs_promises = require("fs/promises");
|
|
6
6
|
//#region src/scanEntityFiles.ts
|
|
7
7
|
const regex = /config\.setDefault\([\s\n]*["'](\w+)["']/g;
|
|
8
|
-
|
|
8
|
+
const transitionsRegex = /config\.(?:setTransitions|addTransitionRule|addTransitionReaction)\(\s*(?:(?:["'][^"']*["']|\w+)\s*,\s*(?=["']))?["'](\w+)["']/g;
|
|
9
|
+
/** Scans the entity files themselves for usage hints (like `setDefault` or `setTransitions` calls) to drive our codegen output. */
|
|
9
10
|
async function scanEntityFiles(config, dbMeta) {
|
|
10
11
|
await Promise.all(dbMeta.entities.map(async (entity) => {
|
|
11
12
|
try {
|
|
@@ -15,6 +16,10 @@ async function scanEntityFiles(config, dbMeta) {
|
|
|
15
16
|
const field = defaultableFields.find((f) => f.fieldName === match[1]);
|
|
16
17
|
if (field) field.hasConfigDefault = true;
|
|
17
18
|
}
|
|
19
|
+
for (const match of tsCode.matchAll(transitionsRegex)) {
|
|
20
|
+
const field = defaultableFields.find((f) => f.fieldName === match[1]);
|
|
21
|
+
if (field?.kind === "enum") field.hasTransitions = true;
|
|
22
|
+
}
|
|
18
23
|
} catch (e) {}
|
|
19
24
|
}));
|
|
20
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanEntityFiles.cjs","names":["readFile","join"],"sources":["../src/scanEntityFiles.ts"],"sourcesContent":["import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { type Config } from \"./config.ts\";\nimport {\n type DbMetadata,\n type EntityDbMetadata,\n type EnumField,\n type ManyToOneField,\n type PgEnumField,\n type PolymorphicField,\n type PrimitiveField,\n} from \"./EntityDbMetadata.ts\";\n\n// Erg, we need a regex in case the fieldName arg is wrapped onto a new line... :-/\nconst regex = /config\\.setDefault\\([\\s\\n]*[\"'](\\w+)[\"']/g;\n\n/** Scans the entity files themselves for usage hints (like `setDefault` calls) to drive our codegen output. */\nexport async function scanEntityFiles(config: Config, dbMeta: DbMetadata): Promise<void> {\n await Promise.all(\n dbMeta.entities.map(async (entity) => {\n try {\n // load the entity.ts file (as a promise with fs/promises)\n const tsCode = (await readFile(join(config.entitiesDirectory, `${entity.name}.ts`))).toString();\n const defaultableFields = recursiveDefaultableFields(dbMeta, entity);\n for (const match of tsCode.matchAll(regex)) {\n const field = defaultableFields.find((f) => f.fieldName === match[1]);\n if (field) {\n field.hasConfigDefault = true;\n }\n }\n } catch (e) {\n // Ignore file not existing yet\n }\n }),\n );\n}\n\n/**\n * Traverse the entity inheritance tree to find all fields that can have defaults set on them.\n *\n * I.e. a column defined in a base CTI table like `publishers.foo_bar` having a `config.setDefault` in\n * `SmallPublisher` and/or `LargePublisher`, or the same scenario for an STI table like `tasks.foo_bar`\n * with `TaskNew` and/or `TaskOld`.\n *\n * NOTE: I suspect here could be some edge cases here that will need to be ironed out, but should work\n * for simple inheritance structures. In particular, there may be unexpected side effects if a base\n * class field has a default set in one subtype, but not another.\n */\nfunction recursiveDefaultableFields(\n dbMeta: DbMetadata,\n entity: EntityDbMetadata | undefined,\n): Array<PrimitiveField | EnumField | PgEnumField | ManyToOneField | PolymorphicField> {\n if (!entity) return [];\n return [\n ...entity.primitives,\n ...entity.enums,\n ...entity.pgEnums,\n ...entity.manyToOnes,\n ...entity.polymorphics,\n ...(entity.baseClassName ? recursiveDefaultableFields(dbMeta, dbMeta.entitiesByName[entity.baseClassName]) : []),\n ];\n}\n"],"mappings":";;;;;;AAeA,MAAM,QAAQ;;
|
|
1
|
+
{"version":3,"file":"scanEntityFiles.cjs","names":["readFile","join"],"sources":["../src/scanEntityFiles.ts"],"sourcesContent":["import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { type Config } from \"./config.ts\";\nimport {\n type DbMetadata,\n type EntityDbMetadata,\n type EnumField,\n type ManyToOneField,\n type PgEnumField,\n type PolymorphicField,\n type PrimitiveField,\n} from \"./EntityDbMetadata.ts\";\n\n// Erg, we need a regex in case the fieldName arg is wrapped onto a new line... :-/\nconst regex = /config\\.setDefault\\([\\s\\n]*[\"'](\\w+)[\"']/g;\n// Any transition method marks the field; addTransitionReaction may have a name before the field argument.\nconst transitionsRegex =\n /config\\.(?:setTransitions|addTransitionRule|addTransitionReaction)\\(\\s*(?:(?:[\"'][^\"']*[\"']|\\w+)\\s*,\\s*(?=[\"']))?[\"'](\\w+)[\"']/g;\n\n/** Scans the entity files themselves for usage hints (like `setDefault` or `setTransitions` calls) to drive our codegen output. */\nexport async function scanEntityFiles(config: Config, dbMeta: DbMetadata): Promise<void> {\n await Promise.all(\n dbMeta.entities.map(async (entity) => {\n try {\n // load the entity.ts file (as a promise with fs/promises)\n const tsCode = (await readFile(join(config.entitiesDirectory, `${entity.name}.ts`))).toString();\n const defaultableFields = recursiveDefaultableFields(dbMeta, entity);\n for (const match of tsCode.matchAll(regex)) {\n const field = defaultableFields.find((f) => f.fieldName === match[1]);\n if (field) {\n field.hasConfigDefault = true;\n }\n }\n // Transition fields get `withStatus` factory opts and accessor names, i.e. `Draft`\n for (const match of tsCode.matchAll(transitionsRegex)) {\n const field = defaultableFields.find((f) => f.fieldName === match[1]);\n if (field?.kind === \"enum\") {\n field.hasTransitions = true;\n }\n }\n } catch (e) {\n // Ignore file not existing yet\n }\n }),\n );\n}\n\n/**\n * Traverse the entity inheritance tree to find all fields that can have defaults set on them.\n *\n * I.e. a column defined in a base CTI table like `publishers.foo_bar` having a `config.setDefault` in\n * `SmallPublisher` and/or `LargePublisher`, or the same scenario for an STI table like `tasks.foo_bar`\n * with `TaskNew` and/or `TaskOld`.\n *\n * NOTE: I suspect here could be some edge cases here that will need to be ironed out, but should work\n * for simple inheritance structures. In particular, there may be unexpected side effects if a base\n * class field has a default set in one subtype, but not another.\n */\nfunction recursiveDefaultableFields(\n dbMeta: DbMetadata,\n entity: EntityDbMetadata | undefined,\n): Array<PrimitiveField | EnumField | PgEnumField | ManyToOneField | PolymorphicField> {\n if (!entity) return [];\n return [\n ...entity.primitives,\n ...entity.enums,\n ...entity.pgEnums,\n ...entity.manyToOnes,\n ...entity.polymorphics,\n ...(entity.baseClassName ? recursiveDefaultableFields(dbMeta, dbMeta.entitiesByName[entity.baseClassName]) : []),\n ];\n}\n"],"mappings":";;;;;;AAeA,MAAM,QAAQ;AAEd,MAAM,mBACJ;;AAGF,eAAsB,gBAAgB,QAAgB,QAAmC;CACvF,MAAM,QAAQ,IACZ,OAAO,SAAS,IAAI,OAAO,WAAW;EACpC,IAAI;GAEF,MAAM,UAAU,OAAA,GAAMA,YAAAA,SAAAA,EAAAA,GAASC,KAAAA,KAAAA,CAAK,OAAO,mBAAmB,GAAG,OAAO,KAAK,IAAI,CAAC,EAAA,CAAG,SAAS;GAC9F,MAAM,oBAAoB,2BAA2B,QAAQ,MAAM;GACnE,KAAK,MAAM,SAAS,OAAO,SAAS,KAAK,GAAG;IAC1C,MAAM,QAAQ,kBAAkB,MAAM,MAAM,EAAE,cAAc,MAAM,EAAE;IACpE,IAAI,OACF,MAAM,mBAAmB;GAE7B;GAEA,KAAK,MAAM,SAAS,OAAO,SAAS,gBAAgB,GAAG;IACrD,MAAM,QAAQ,kBAAkB,MAAM,MAAM,EAAE,cAAc,MAAM,EAAE;IACpE,IAAI,OAAO,SAAS,QAClB,MAAM,iBAAiB;GAE3B;EACF,SAAS,GAAG,CAEZ;CACF,CAAC,CACH;AACF;;;;;;;;;;;;AAaA,SAAS,2BACP,QACA,QACqF;CACrF,IAAI,CAAC,QAAQ,OAAO,CAAC;CACrB,OAAO;EACL,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAI,OAAO,gBAAgB,2BAA2B,QAAQ,OAAO,eAAe,OAAO,cAAc,IAAI,CAAC;CAChH;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Config } from "./config.cjs";
|
|
2
2
|
import { DbMetadata } from "./EntityDbMetadata.cjs";
|
|
3
3
|
//#region src/scanEntityFiles.d.ts
|
|
4
|
-
/** Scans the entity files themselves for usage hints (like `setDefault` calls) to drive our codegen output. */
|
|
4
|
+
/** Scans the entity files themselves for usage hints (like `setDefault` or `setTransitions` calls) to drive our codegen output. */
|
|
5
5
|
declare function scanEntityFiles(config: Config, dbMeta: DbMetadata): Promise<void>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { scanEntityFiles };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanEntityFiles.d.cts","names":[],"sources":["../src/scanEntityFiles.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"scanEntityFiles.d.cts","names":[],"sources":["../src/scanEntityFiles.ts"],"mappings":";;;;iBAqBsB,gBAAgB,QAAQ,QAAQ,QAAQ,aAAa"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Config } from "./config.mjs";
|
|
2
2
|
import { DbMetadata } from "./EntityDbMetadata.mjs";
|
|
3
3
|
//#region src/scanEntityFiles.d.ts
|
|
4
|
-
/** Scans the entity files themselves for usage hints (like `setDefault` calls) to drive our codegen output. */
|
|
4
|
+
/** Scans the entity files themselves for usage hints (like `setDefault` or `setTransitions` calls) to drive our codegen output. */
|
|
5
5
|
declare function scanEntityFiles(config: Config, dbMeta: DbMetadata): Promise<void>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { scanEntityFiles };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanEntityFiles.d.mts","names":[],"sources":["../src/scanEntityFiles.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"scanEntityFiles.d.mts","names":[],"sources":["../src/scanEntityFiles.ts"],"mappings":";;;;iBAqBsB,gBAAgB,QAAQ,QAAQ,QAAQ,aAAa"}
|
package/build/scanEntityFiles.js
CHANGED
|
@@ -4,7 +4,8 @@ import { join } from "path";
|
|
|
4
4
|
import { readFile } from "fs/promises";
|
|
5
5
|
//#region src/scanEntityFiles.ts
|
|
6
6
|
const regex = /config\.setDefault\([\s\n]*["'](\w+)["']/g;
|
|
7
|
-
|
|
7
|
+
const transitionsRegex = /config\.(?:setTransitions|addTransitionRule|addTransitionReaction)\(\s*(?:(?:["'][^"']*["']|\w+)\s*,\s*(?=["']))?["'](\w+)["']/g;
|
|
8
|
+
/** Scans the entity files themselves for usage hints (like `setDefault` or `setTransitions` calls) to drive our codegen output. */
|
|
8
9
|
async function scanEntityFiles(config, dbMeta) {
|
|
9
10
|
await Promise.all(dbMeta.entities.map(async (entity) => {
|
|
10
11
|
try {
|
|
@@ -14,6 +15,10 @@ async function scanEntityFiles(config, dbMeta) {
|
|
|
14
15
|
const field = defaultableFields.find((f) => f.fieldName === match[1]);
|
|
15
16
|
if (field) field.hasConfigDefault = true;
|
|
16
17
|
}
|
|
18
|
+
for (const match of tsCode.matchAll(transitionsRegex)) {
|
|
19
|
+
const field = defaultableFields.find((f) => f.fieldName === match[1]);
|
|
20
|
+
if (field?.kind === "enum") field.hasTransitions = true;
|
|
21
|
+
}
|
|
17
22
|
} catch (e) {}
|
|
18
23
|
}));
|
|
19
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanEntityFiles.js","names":[],"sources":["../src/scanEntityFiles.ts"],"sourcesContent":["import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { type Config } from \"./config.ts\";\nimport {\n type DbMetadata,\n type EntityDbMetadata,\n type EnumField,\n type ManyToOneField,\n type PgEnumField,\n type PolymorphicField,\n type PrimitiveField,\n} from \"./EntityDbMetadata.ts\";\n\n// Erg, we need a regex in case the fieldName arg is wrapped onto a new line... :-/\nconst regex = /config\\.setDefault\\([\\s\\n]*[\"'](\\w+)[\"']/g;\n\n/** Scans the entity files themselves for usage hints (like `setDefault` calls) to drive our codegen output. */\nexport async function scanEntityFiles(config: Config, dbMeta: DbMetadata): Promise<void> {\n await Promise.all(\n dbMeta.entities.map(async (entity) => {\n try {\n // load the entity.ts file (as a promise with fs/promises)\n const tsCode = (await readFile(join(config.entitiesDirectory, `${entity.name}.ts`))).toString();\n const defaultableFields = recursiveDefaultableFields(dbMeta, entity);\n for (const match of tsCode.matchAll(regex)) {\n const field = defaultableFields.find((f) => f.fieldName === match[1]);\n if (field) {\n field.hasConfigDefault = true;\n }\n }\n } catch (e) {\n // Ignore file not existing yet\n }\n }),\n );\n}\n\n/**\n * Traverse the entity inheritance tree to find all fields that can have defaults set on them.\n *\n * I.e. a column defined in a base CTI table like `publishers.foo_bar` having a `config.setDefault` in\n * `SmallPublisher` and/or `LargePublisher`, or the same scenario for an STI table like `tasks.foo_bar`\n * with `TaskNew` and/or `TaskOld`.\n *\n * NOTE: I suspect here could be some edge cases here that will need to be ironed out, but should work\n * for simple inheritance structures. In particular, there may be unexpected side effects if a base\n * class field has a default set in one subtype, but not another.\n */\nfunction recursiveDefaultableFields(\n dbMeta: DbMetadata,\n entity: EntityDbMetadata | undefined,\n): Array<PrimitiveField | EnumField | PgEnumField | ManyToOneField | PolymorphicField> {\n if (!entity) return [];\n return [\n ...entity.primitives,\n ...entity.enums,\n ...entity.pgEnums,\n ...entity.manyToOnes,\n ...entity.polymorphics,\n ...(entity.baseClassName ? recursiveDefaultableFields(dbMeta, dbMeta.entitiesByName[entity.baseClassName]) : []),\n ];\n}\n"],"mappings":";;;;;AAeA,MAAM,QAAQ;;
|
|
1
|
+
{"version":3,"file":"scanEntityFiles.js","names":[],"sources":["../src/scanEntityFiles.ts"],"sourcesContent":["import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { type Config } from \"./config.ts\";\nimport {\n type DbMetadata,\n type EntityDbMetadata,\n type EnumField,\n type ManyToOneField,\n type PgEnumField,\n type PolymorphicField,\n type PrimitiveField,\n} from \"./EntityDbMetadata.ts\";\n\n// Erg, we need a regex in case the fieldName arg is wrapped onto a new line... :-/\nconst regex = /config\\.setDefault\\([\\s\\n]*[\"'](\\w+)[\"']/g;\n// Any transition method marks the field; addTransitionReaction may have a name before the field argument.\nconst transitionsRegex =\n /config\\.(?:setTransitions|addTransitionRule|addTransitionReaction)\\(\\s*(?:(?:[\"'][^\"']*[\"']|\\w+)\\s*,\\s*(?=[\"']))?[\"'](\\w+)[\"']/g;\n\n/** Scans the entity files themselves for usage hints (like `setDefault` or `setTransitions` calls) to drive our codegen output. */\nexport async function scanEntityFiles(config: Config, dbMeta: DbMetadata): Promise<void> {\n await Promise.all(\n dbMeta.entities.map(async (entity) => {\n try {\n // load the entity.ts file (as a promise with fs/promises)\n const tsCode = (await readFile(join(config.entitiesDirectory, `${entity.name}.ts`))).toString();\n const defaultableFields = recursiveDefaultableFields(dbMeta, entity);\n for (const match of tsCode.matchAll(regex)) {\n const field = defaultableFields.find((f) => f.fieldName === match[1]);\n if (field) {\n field.hasConfigDefault = true;\n }\n }\n // Transition fields get `withStatus` factory opts and accessor names, i.e. `Draft`\n for (const match of tsCode.matchAll(transitionsRegex)) {\n const field = defaultableFields.find((f) => f.fieldName === match[1]);\n if (field?.kind === \"enum\") {\n field.hasTransitions = true;\n }\n }\n } catch (e) {\n // Ignore file not existing yet\n }\n }),\n );\n}\n\n/**\n * Traverse the entity inheritance tree to find all fields that can have defaults set on them.\n *\n * I.e. a column defined in a base CTI table like `publishers.foo_bar` having a `config.setDefault` in\n * `SmallPublisher` and/or `LargePublisher`, or the same scenario for an STI table like `tasks.foo_bar`\n * with `TaskNew` and/or `TaskOld`.\n *\n * NOTE: I suspect here could be some edge cases here that will need to be ironed out, but should work\n * for simple inheritance structures. In particular, there may be unexpected side effects if a base\n * class field has a default set in one subtype, but not another.\n */\nfunction recursiveDefaultableFields(\n dbMeta: DbMetadata,\n entity: EntityDbMetadata | undefined,\n): Array<PrimitiveField | EnumField | PgEnumField | ManyToOneField | PolymorphicField> {\n if (!entity) return [];\n return [\n ...entity.primitives,\n ...entity.enums,\n ...entity.pgEnums,\n ...entity.manyToOnes,\n ...entity.polymorphics,\n ...(entity.baseClassName ? recursiveDefaultableFields(dbMeta, dbMeta.entitiesByName[entity.baseClassName]) : []),\n ];\n}\n"],"mappings":";;;;;AAeA,MAAM,QAAQ;AAEd,MAAM,mBACJ;;AAGF,eAAsB,gBAAgB,QAAgB,QAAmC;CACvF,MAAM,QAAQ,IACZ,OAAO,SAAS,IAAI,OAAO,WAAW;EACpC,IAAI;GAEF,MAAM,UAAU,MAAM,SAAS,KAAK,OAAO,mBAAmB,GAAG,OAAO,KAAK,IAAI,CAAC,EAAA,CAAG,SAAS;GAC9F,MAAM,oBAAoB,2BAA2B,QAAQ,MAAM;GACnE,KAAK,MAAM,SAAS,OAAO,SAAS,KAAK,GAAG;IAC1C,MAAM,QAAQ,kBAAkB,MAAM,MAAM,EAAE,cAAc,MAAM,EAAE;IACpE,IAAI,OACF,MAAM,mBAAmB;GAE7B;GAEA,KAAK,MAAM,SAAS,OAAO,SAAS,gBAAgB,GAAG;IACrD,MAAM,QAAQ,kBAAkB,MAAM,MAAM,EAAE,cAAc,MAAM,EAAE;IACpE,IAAI,OAAO,SAAS,QAClB,MAAM,iBAAiB;GAE3B;EACF,SAAS,GAAG,CAEZ;CACF,CAAC,CACH;AACF;;;;;;;;;;;;AAaA,SAAS,2BACP,QACA,QACqF;CACrF,IAAI,CAAC,QAAQ,OAAO,CAAC;CACrB,OAAO;EACL,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAI,OAAO,gBAAgB,2BAA2B,QAAQ,OAAO,eAAe,OAAO,cAAc,IAAI,CAAC;CAChH;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joist-codegen",
|
|
3
|
-
"version": "2.4.0-next.
|
|
3
|
+
"version": "2.4.0-next.31",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"skills"
|
|
49
49
|
],
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"joist-utils": "2.4.0-next.
|
|
51
|
+
"joist-utils": "2.4.0-next.31",
|
|
52
52
|
"knex": "^3.1.0",
|
|
53
53
|
"pg": "^8.23.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@dprint/formatter": "^0.5.1",
|
|
57
|
-
"@dprint/json": "^0.
|
|
58
|
-
"@inquirer/prompts": "^8.
|
|
57
|
+
"@dprint/json": "^0.25.1",
|
|
58
|
+
"@inquirer/prompts": "^8.7.2",
|
|
59
59
|
"@types/jscodeshift": "^17.3.0",
|
|
60
60
|
"@types/pluralize": "^0.0.33",
|
|
61
61
|
"change-case": "^4.1.2",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"jscodeshift": "^17.4.0",
|
|
64
64
|
"jsonc-parser": "^3.3.1",
|
|
65
65
|
"pluralize": "^8.0.0",
|
|
66
|
-
"prettier": "^3.9.
|
|
66
|
+
"prettier": "^3.9.9",
|
|
67
67
|
"ts-poet": "^6.12.0",
|
|
68
|
-
"zod": "^4.
|
|
68
|
+
"zod": "^4.6.5"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@swc/jest": "^0.2.39",
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: joist-test-factories
|
|
3
|
-
description: Use when writing or refactoring Joist ORM tests with newTestInstance factories, DeepNew,
|
|
3
|
+
description: Use when writing or refactoring Joist ORM entity or API-layer tests with newTestInstance factories, DeepNew, toMatchEntity, and nested Given graphs, or deciding whether EntityManager isolation with run, makeRun, or RunPlugin is needed.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Joist Test Factories
|
|
7
7
|
|
|
8
|
-
Write
|
|
8
|
+
Write focused, succinct tests that use Joist's factories for setup, retain
|
|
9
9
|
typed entity references through the action, and assert those same entities
|
|
10
10
|
with `toMatchEntity`.
|
|
11
11
|
|
|
12
|
+
Entity tests normally use one `EntityManager` for Given, When, and Then.
|
|
13
|
+
Use `run` for API-layer tests and other production boundaries that need a
|
|
14
|
+
separate `EntityManager`, not as a default wrapper around every test action.
|
|
15
|
+
|
|
12
16
|
Authoritative references:
|
|
13
17
|
|
|
14
18
|
- <https://joist-orm.io/testing/test-factories/>
|
|
@@ -27,10 +31,16 @@ Authoritative references:
|
|
|
27
31
|
4. Keep using the factory-created `DeepNew` graph after same-`EntityManager`
|
|
28
32
|
actions or actions run through Joist's test `run` helper. Do not reload the
|
|
29
33
|
same rows merely to assert against them.
|
|
30
|
-
5.
|
|
34
|
+
5. Keep entity tests in the original test `EntityManager`; they normally do not
|
|
35
|
+
need EM isolation or `RunPlugin`. Use `run` or the project's `makeRun` wrapper
|
|
36
|
+
for API/request-layer tests or another production boundary that needs an
|
|
31
37
|
isolated `EntityManager`; it flushes Given state and mirrors the callback's
|
|
32
38
|
flushed Joist writes into the original test graph.
|
|
33
|
-
|
|
39
|
+
Do not `await em.flush()` right before `run` or a `makeRun` helper: it
|
|
40
|
+
already flushes the Given graph, use as few `em.flush()` calls as possible.
|
|
41
|
+
6. Assert entity state and relationships with `toMatchEntity`. Prefer checking
|
|
42
|
+
side effects through retained entities and their relations instead of using
|
|
43
|
+
`em.getEntities` to discover what the action created or changed.
|
|
34
44
|
7. Prefer focused tests for one behavior over a single scenario that exercises
|
|
35
45
|
unrelated updates at several graph levels.
|
|
36
46
|
|
|
@@ -41,6 +51,23 @@ happens, Then the state of the world is Z": the factory-created graph is the
|
|
|
41
51
|
Given, the code under test is the When, and `toMatchEntity` proves the Then.
|
|
42
52
|
Mark the three phases with `// Given`, `// When`, `// Then` comments:
|
|
43
53
|
|
|
54
|
+
For an entity test, act directly on the factory-created entity:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
it.withCtx("updates a book's title", async (ctx) => {
|
|
58
|
+
// Given a book with its original title
|
|
59
|
+
const book = newBook(ctx.em, { title: "Before" });
|
|
60
|
+
|
|
61
|
+
// When we change the book's title
|
|
62
|
+
book.title = "After";
|
|
63
|
+
|
|
64
|
+
// Then the book has the new title
|
|
65
|
+
expect(book).toMatchEntity({ title: "After" });
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For an API-layer test, use `run` to model its separate production unit of work:
|
|
70
|
+
|
|
44
71
|
```ts
|
|
45
72
|
it.withCtx("updates a book", async (ctx) => {
|
|
46
73
|
// Given an author with one book
|
|
@@ -241,6 +268,23 @@ test-only loads.
|
|
|
241
268
|
|
|
242
269
|
## Use run for Production Isolation
|
|
243
270
|
|
|
271
|
+
Choose isolation based on the behavior under test:
|
|
272
|
+
|
|
273
|
+
- **Entity tests:** Test entity methods, properties, relationships, and domain
|
|
274
|
+
logic directly with the test's `ctx.em` and factory-created entities. These
|
|
275
|
+
normally do not need `run`, a second EM, or a manually installed `RunPlugin`.
|
|
276
|
+
Flush only when the behavior requires it, such as validation, flush hooks,
|
|
277
|
+
persisted reactive fields, or database constraints. Needing a flush does not
|
|
278
|
+
by itself mean the test needs EM isolation.
|
|
279
|
+
- **API/request-layer tests:** Use `run` or an existing project wrapper such as
|
|
280
|
+
`runMutation` when testing a resolver, endpoint, or service entry point whose
|
|
281
|
+
production unit of work uses a fresh EM. This ensures the action loads its
|
|
282
|
+
own entities rather than inheriting the test's loaded factory graph.
|
|
283
|
+
- **Other boundaries:** Use `run` when a fresh production unit of work is part
|
|
284
|
+
of the behavior being tested. A service/helper that operates on existing
|
|
285
|
+
entities or the caller's EM does not need isolation merely because it is
|
|
286
|
+
production code.
|
|
287
|
+
|
|
244
288
|
Joist's `run(ctx, fn)` executes `fn` with a fresh production-style context and
|
|
245
289
|
`EntityManager`. Before the callback, it flushes the test factory graph. As the
|
|
246
290
|
callback flushes Joist writes, `RunPlugin` mirrors those writes into the
|
|
@@ -248,6 +292,11 @@ original test `EntityManager`. It does not call `EntityManager.refresh`. The
|
|
|
248
292
|
callback result is also mapped back to entities from the original test
|
|
249
293
|
`EntityManager`.
|
|
250
294
|
|
|
295
|
+
`run` installs `RunPlugin` automatically on the isolated EM. The plugin bridges
|
|
296
|
+
that production unit of work back to the retained test entities; it is not
|
|
297
|
+
needed when Given, When, and Then already share one EM. Prefer `run` or the
|
|
298
|
+
project's wrapper over manually wiring the plugin.
|
|
299
|
+
|
|
251
300
|
```ts
|
|
252
301
|
const author = newAuthor(ctx.em, { books: [{ title: "Before" }] });
|
|
253
302
|
const [book] = author.books.get;
|
|
@@ -278,8 +327,9 @@ const books = await savedBook.author.load().then((author) => author.books.load()
|
|
|
278
327
|
Keep using `author` and `book`. They are the stable test references.
|
|
279
328
|
|
|
280
329
|
Call `em.flush()` directly only when the test intentionally needs a persistence
|
|
281
|
-
boundary and its helper does not provide one.
|
|
282
|
-
|
|
330
|
+
boundary and its helper does not provide one, e.g. to persist one Given state
|
|
331
|
+
before changing it into the next one. Pure entity tests should usually avoid
|
|
332
|
+
unnecessary flushes.
|
|
283
333
|
|
|
284
334
|
## Register toMatchEntity
|
|
285
335
|
|
|
@@ -338,6 +388,39 @@ expect((await author.books.load()).map((book) => ({ id: book.id, title: book.tit
|
|
|
338
388
|
]);
|
|
339
389
|
```
|
|
340
390
|
|
|
391
|
+
## Assert Side Effects Through the Domain Graph
|
|
392
|
+
|
|
393
|
+
Avoid `em.getEntities(Entity).filter(...)` or `.find(...)` to look up side
|
|
394
|
+
effects for assertions. `getEntities` inspects the `EntityManager`'s tracked
|
|
395
|
+
entities; finding a new child there does not prove that the action attached it
|
|
396
|
+
to the correct parent. Assert through the retained Given graph instead, so the
|
|
397
|
+
test describes the resulting domain state and checks relationship membership.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
// Less useful: finds a book without proving which author owns it.
|
|
401
|
+
const [createdBook] = ctx.em.getEntities(Book).filter((book) => book.title === "New book");
|
|
402
|
+
expect(createdBook).toMatchEntity({ title: "New book" });
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
it.withCtx("adds a book to the existing author", async (ctx) => {
|
|
407
|
+
// Given an author with an existing book
|
|
408
|
+
const author = newAuthor(ctx.em, { books: [{}] });
|
|
409
|
+
const [existingBook] = author.books.get;
|
|
410
|
+
|
|
411
|
+
// When we add another book to that author
|
|
412
|
+
await run(ctx, (ctx) => addBook(ctx, { authorId: author.id, title: "New book" }));
|
|
413
|
+
|
|
414
|
+
// Then the author keeps the existing book and owns the new book
|
|
415
|
+
expect(author).toMatchEntity({ books: [existingBook, { title: "New book" }] });
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Use the action result when a new entity has no natural path from the Given
|
|
420
|
+
graph. If neither a relation nor the result exposes it, use an intentional
|
|
421
|
+
domain query. Reserve `getEntities` assertions for tests whose behavior under
|
|
422
|
+
test is specifically which entities the `EntityManager` tracks.
|
|
423
|
+
|
|
341
424
|
## Keep Tests Focused
|
|
342
425
|
|
|
343
426
|
One test should describe one coherent boundary. Split a large graph mutation
|
|
@@ -390,8 +473,12 @@ reuse decisions instead of replacing factories with manual setup.
|
|
|
390
473
|
- Given state uses minimal factory opts and does not invoke the behavior under
|
|
391
474
|
test.
|
|
392
475
|
- Important entities have direct, role-based `const` names.
|
|
476
|
+
- Entity tests use the original test EM unless the behavior specifically needs
|
|
477
|
+
a separate production unit of work; a flush alone does not require `run`.
|
|
393
478
|
- Separate production units of work use `run`/`makeRun` and flush normally.
|
|
394
479
|
- Assertions reuse loaded factory entities with `toMatchEntity`.
|
|
480
|
+
- Side-effect assertions follow domain relations or action results instead of
|
|
481
|
+
scanning `em.getEntities` for created or changed entities.
|
|
395
482
|
- No unnecessary reloads, `load()` calls, assertion awaits, or direct flushes
|
|
396
483
|
remain.
|
|
397
484
|
- Each test covers one coherent boundary.
|