@compas/code-gen
Version:
Generate various boring parts of your server
288 lines (259 loc) • 8 kB
JavaScript
import { AppError } from "@compas/stdlib";
import {
fileContextCreateGeneric,
fileContextSetIndent,
} from "../file/context.js";
import { fileFormatInlineComment } from "../file/docs.js";
import { fileWrite, fileWriteInline } from "../file/write.js";
import {
modelKeyGetPrimary,
modelKeyGetSearchable,
} from "../processors/model-keys.js";
import {
modelRelationGetInformation,
modelRelationGetOwn,
} from "../processors/model-relation.js";
import { structureModels } from "../processors/models.js";
import { referenceUtilsGetProperty } from "../processors/reference-utils.js";
import { structureResolveReference } from "../processors/structure.js";
import { typesOptionalityIsOptional } from "../types/optionality.js";
import { stringFormatNameForError, upperCaseFirst } from "../utils.js";
/**
* Write the DDL out for Postgres
*
* @param {import("../generate.js").GenerateContext} generateContext
*/
export function databasePostgresWriteDDL(generateContext) {
const file = fileContextCreateGeneric(
generateContext,
"common/structure.sql",
{
inlineCommentPrefix: "-- ",
},
);
databasePostgresWriteWarning(file);
for (const model of structureModels(generateContext)) {
databasePostgresWriteModelDDL(generateContext, file, model);
}
}
/**
* @param {import("../file/context.js").GenerateFile} file
*/
function databasePostgresWriteWarning(file) {
fileWrite(
file,
fileFormatInlineComment(
file,
`WARNING:
This file is a suggestion, and can be used to create your migration files.
Please note that many indexes can have a negative impact on performance. Combining indexes may also be a good option in some circumstances.
The order of this output is alphabetically sorted, dependencies between tables are not taken into account.`,
),
);
fileWrite(file, `\n\n`);
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureObjectDefinition>} model
*/
function databasePostgresWriteModelDDL(generateContext, file, model) {
if (model.queryOptions?.isView) {
fileWrite(
file,
fileFormatInlineComment(file, `Note: Views are not supported.`),
);
fileWrite(
file,
fileFormatInlineComment(
file,
`CREATE OR REPLACE VIEW "${model.name}" AS SELECT 1 + 1 as "column";`,
),
);
fileWrite(
file,
fileFormatInlineComment(
file,
`Required columns: ${JSON.stringify(Object.keys(model.keys)).slice(1, -1)}`,
),
);
fileWrite(file, "");
return;
}
fileWrite(file, `CREATE TABLE "${model.name}"`);
fileWrite(file, `(`);
fileContextSetIndent(file, 1);
let hasWrittenALine = false;
for (const key of Object.keys(model.keys)) {
const field = model.keys[key];
if (hasWrittenALine) {
fileWrite(file, ",");
} else {
hasWrittenALine = true;
}
const type =
field.type === "reference" ?
structureResolveReference(generateContext.structure, field).type
: field.type;
const isPrimary = referenceUtilsGetProperty(generateContext, field, [
"sql",
"primary",
]);
const hasDefaultValue = referenceUtilsGetProperty(generateContext, field, [
"sql",
"hasDefaultValue",
]);
const isOptional = typesOptionalityIsOptional(generateContext, field, {
validatorState: "output",
});
fileWriteInline(file, `"${key}"`);
if (isPrimary) {
switch (
/** @type {import("../generated/common/types.js").StructureTypeSystemDefinition["type"]} */ type
) {
case "number":
fileWriteInline(file, ` BIGSERIAL PRIMARY KEY`);
break;
case "string":
fileWriteInline(file, ` varchar PRIMARY KEY`);
break;
case "uuid":
fileWriteInline(file, ` uuid PRIMARY KEY`);
break;
default:
throw AppError.serverError({
message: "Can't generate primary key for the provided type",
model: stringFormatNameForError(model),
key,
type,
});
}
} else {
switch (
/** @type {import("../generated/common/types.js").StructureTypeSystemDefinition["type"]} */ type
) {
case "any":
fileWriteInline(file, ` jsonb`);
break;
case "anyOf":
fileWriteInline(file, ` jsonb`);
break;
case "array":
fileWriteInline(file, ` jsonb`);
break;
case "boolean":
fileWriteInline(file, ` boolean`);
break;
case "date":
{
const specifier = referenceUtilsGetProperty(
generateContext,
field,
["specifier"],
);
if (specifier === "dateOnly") {
fileWriteInline(file, ` date`);
} else if (specifier === "timeOnly") {
fileWriteInline(file, ` time`);
} else {
fileWriteInline(file, ` timestamptz`);
}
}
break;
case "generic":
fileWriteInline(file, ` jsonb`);
break;
case "number":
{
const isFloat = referenceUtilsGetProperty(generateContext, field, [
"validator",
"floatingPoint",
]);
if (isFloat) {
fileWriteInline(file, ` float`);
} else {
fileWriteInline(file, ` int`);
}
}
break;
case "object":
fileWriteInline(file, ` jsonb`);
break;
case "string":
fileWriteInline(file, ` varchar`);
break;
case "uuid":
fileWriteInline(file, ` uuid`);
break;
}
}
if (isOptional && !hasDefaultValue) {
fileWriteInline(file, ` NULL`);
} else {
fileWriteInline(file, ` NOT NULL`);
}
if (isPrimary && type === "uuid") {
fileWriteInline(file, ` DEFAULT uuid_generate_v4()`);
} else if (
model.queryOptions?.withDates ||
model.queryOptions?.withSoftDeletes
) {
if (key === "createdAt" || key === "updatedAt") {
fileWriteInline(file, ` DEFAULT now()`);
}
} else if (hasDefaultValue) {
fileWriteInline(
file,
` DEFAULT 'Fill in a valid default based on the column type'`,
);
}
}
for (const relation of modelRelationGetOwn(model)) {
if (hasWrittenALine) {
fileWrite(file, ",");
} else {
hasWrittenALine = true;
}
const relationInfo = modelRelationGetInformation(relation);
fileWriteInline(
file,
`constraint "${model.name}${upperCaseFirst(
relationInfo.keyNameOwn,
)}Fk" foreign key ("${relationInfo.keyNameOwn}") references "${
relationInfo.modelInverse.name
}" ("${relationInfo.primaryKeyNameInverse}")`,
);
if (relation.isOptional) {
fileWriteInline(file, " ON DELETE SET NULL");
} else {
fileWriteInline(file, " ON DELETE CASCADE");
}
}
if (hasWrittenALine) {
fileWrite(file, "");
}
fileContextSetIndent(file, -1);
fileWrite(file, `);\n`);
if (model.queryOptions?.withDates || model.queryOptions?.withSoftDeletes) {
fileWrite(
file,
`CREATE INDEX "${model.name}DatesIdx" ON "${model.name}" ("createdAt", "updatedAt");`,
);
}
const { primaryKeyName } = modelKeyGetPrimary(model);
for (const key of modelKeyGetSearchable(generateContext, model)) {
if (key === "createdAt" || key === "updatedAt") {
continue;
}
if (primaryKeyName === key) {
continue;
}
fileWrite(
file,
`CREATE INDEX "${model.name}${upperCaseFirst(key)}Idx" ON "${
model.name
}" ("${key}");`,
);
}
fileWrite(file, "\n");
}