@contember/schema-utils
Advanced tools
Comparing version 1.3.0-alpha.6 to 1.3.0-alpha.7
export * from './TsDefinitionGenerator'; | ||
export * from './AclDefinitionCodeGenerator'; | ||
export * from './DefinitionCodeGenerator'; | ||
export * from './DefinitionNamingConventions'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -18,2 +18,5 @@ "use strict"; | ||
__exportStar(require("./TsDefinitionGenerator"), exports); | ||
__exportStar(require("./AclDefinitionCodeGenerator"), exports); | ||
__exportStar(require("./DefinitionCodeGenerator"), exports); | ||
__exportStar(require("./DefinitionNamingConventions"), exports); | ||
//# sourceMappingURL=index.js.map |
@@ -1,26 +0,12 @@ | ||
import { Model, Schema } from '@contember/schema'; | ||
import { Schema } from '@contember/schema'; | ||
import { NamingConventions } from '../model'; | ||
/** | ||
* @deprecated use {@link DefinitionCodeGenerator} | ||
*/ | ||
export declare class TsDefinitionGenerator { | ||
private readonly schema; | ||
private readonly conventions; | ||
private static reservedWords; | ||
constructor(schema: Schema, conventions?: NamingConventions); | ||
generate(): string; | ||
private generateEnum; | ||
generateEntity({ entity }: { | ||
entity: Model.Entity; | ||
}): string; | ||
private generateUniqueConstraint; | ||
private generateIndex; | ||
private generateView; | ||
generateField({ entity, field }: { | ||
entity: Model.Entity; | ||
field: Model.AnyField; | ||
}): string | undefined; | ||
private generateColumn; | ||
private formatIdentifier; | ||
private formatLiteral; | ||
private isValidIdentifier; | ||
private isSimpleIdentifier; | ||
} | ||
//# sourceMappingURL=TsDefinitionGenerator.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.TsDefinitionGenerator = void 0; | ||
const schema_1 = require("@contember/schema"); | ||
const model_1 = require("../model"); | ||
const DefinitionCodeGenerator_1 = require("./DefinitionCodeGenerator"); | ||
/** | ||
* @deprecated use {@link DefinitionCodeGenerator} | ||
*/ | ||
class TsDefinitionGenerator { | ||
@@ -12,236 +15,7 @@ constructor(schema, conventions = new model_1.DefaultNamingConventions()) { | ||
generate() { | ||
const enums = Object.entries(this.schema.model.enums).map(([name, values]) => this.generateEnum({ | ||
name, | ||
values, | ||
})).join(''); | ||
const entities = Object.values(this.schema.model.entities).map(entity => this.generateEntity({ entity })).join(''); | ||
return `import { SchemaDefinition as def } from '@contember/schema-definition' | ||
${enums}${entities}`; | ||
const generator = new DefinitionCodeGenerator_1.DefinitionCodeGenerator(this.conventions); | ||
return generator.generate(this.schema); | ||
} | ||
generateEnum({ name, values }) { | ||
return `\nexport const ${this.formatIdentifier(name)} = def.createEnum(${values.map(it => this.formatLiteral(it)).join(', ')})\n`; | ||
} | ||
generateEntity({ entity }) { | ||
const decorators = [ | ||
...Object.values(entity.unique).map(constraint => this.generateUniqueConstraint({ entity, constraint })), | ||
...Object.values(entity.indexes).map(index => this.generateIndex({ entity, index })), | ||
this.generateView({ entity }), | ||
].filter(it => !!it).map(it => `${it}\n`).join(''); | ||
return `\n${decorators}export class ${this.formatIdentifier(entity.name)} { | ||
${Object.values(entity.fields).map(field => this.generateField({ field, entity })).filter(it => !!it).join('\n')} | ||
}\n`; | ||
} | ||
generateUniqueConstraint({ entity, constraint }) { | ||
const defaultName = model_1.NamingHelper.createUniqueConstraintName(entity.name, constraint.fields); | ||
if (defaultName === constraint.name) { | ||
const fieldsList = `${constraint.fields.map(it => this.formatLiteral(it)).join(', ')}`; | ||
return `@def.Unique(${fieldsList})`; | ||
} | ||
return `@def.Unique(${this.formatLiteral(constraint)})`; | ||
} | ||
generateIndex({ entity, index }) { | ||
const defaultName = model_1.NamingHelper.createIndexName(entity.name, index.fields); | ||
if (defaultName === index.name) { | ||
const fieldsList = `${index.fields.map(it => this.formatLiteral(it)).join(', ')}`; | ||
return `@def.Index(${fieldsList})`; | ||
} | ||
return `@def.Index(${this.formatLiteral(index)})`; | ||
} | ||
generateView({ entity }) { | ||
var _a, _b, _c; | ||
if (!entity.view) { | ||
return undefined; | ||
} | ||
const dependenciesExpr = ((_b = (_a = entity.view.dependencies) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 | ||
? `, {\n\tdependencies: () => [${(_c = entity.view.dependencies) === null || _c === void 0 ? void 0 : _c.map(it => this.formatIdentifier(it))}]\n}` | ||
: ''; | ||
return `@def.View(\`${entity.view.sql}\`${dependenciesExpr})`; | ||
} | ||
generateField({ entity, field }) { | ||
const formatRelationFactory = (method, relation) => { | ||
const otherSide = (0, model_1.isInverseRelation)(relation) ? relation.ownedBy : relation.inversedBy; | ||
const otherSideFormatted = otherSide ? `, ${this.formatLiteral(otherSide)}` : ''; | ||
return `${method}(${this.formatIdentifier(relation.target)}${otherSideFormatted})`; | ||
}; | ||
const formatEnumRef = (enumName, enumValues, providedValue, defaultValue) => { | ||
var _a; | ||
if (!providedValue || providedValue === defaultValue) { | ||
return undefined; | ||
} | ||
const enumValueKey = (_a = Object.entries(schema_1.Model.OrderDirection).find(dir => dir[1] === providedValue)) === null || _a === void 0 ? void 0 : _a[0]; | ||
if (!enumValueKey) { | ||
throw new Error(`Value ${providedValue} is not defined in enum ${enumName}`); | ||
} | ||
return `${enumName}.${enumValueKey}`; | ||
}; | ||
const formatOrderBy = (orderBy) => { | ||
var _a; | ||
return (_a = orderBy === null || orderBy === void 0 ? void 0 : orderBy.map(it => { | ||
const enumExpr = formatEnumRef(`Model.OrderDirection`, schema_1.Model.OrderDirection, it.direction, schema_1.Model.OrderDirection.asc); | ||
return `orderBy(${this.formatLiteral(it.path)}${enumExpr ? `, ${enumExpr}` : ''})`; | ||
})) !== null && _a !== void 0 ? _a : []; | ||
}; | ||
const formatOnDelete = (onDelete) => { | ||
if (onDelete === schema_1.Model.OnDelete.cascade) { | ||
return 'cascadeOnDelete()'; | ||
} | ||
if (onDelete === schema_1.Model.OnDelete.setNull) { | ||
return 'setNullOnDelete()'; | ||
} | ||
return undefined; | ||
}; | ||
const formatJoiningColumn = (joiningColumnName, fieldName) => { | ||
const defaultJoiningColumn = this.conventions.getJoiningColumnName(fieldName); | ||
if (defaultJoiningColumn === joiningColumnName) { | ||
return undefined; | ||
} | ||
return `joiningColumn(${this.formatLiteral(joiningColumnName)})`; | ||
}; | ||
const definition = (0, model_1.acceptFieldVisitor)(this.schema.model, entity, field, { | ||
visitColumn: ctx => { | ||
return this.generateColumn(ctx); | ||
}, | ||
visitOneHasMany: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('oneHasMany', relation), | ||
...formatOrderBy(relation.orderBy), | ||
]; | ||
}, | ||
visitManyHasOne: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('manyHasOne', relation), | ||
!relation.nullable ? 'notNull()' : undefined, | ||
formatOnDelete(relation.joiningColumn.onDelete), | ||
formatJoiningColumn(relation.joiningColumn.columnName, relation.name), | ||
]; | ||
}, | ||
visitOneHasOneInverse: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('oneHasOneInverse', relation), | ||
!relation.nullable ? 'notNull()' : undefined, | ||
]; | ||
}, | ||
visitOneHasOneOwning: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('oneHasOne', relation), | ||
!relation.nullable ? 'notNull()' : undefined, | ||
formatOnDelete(relation.joiningColumn.onDelete), | ||
formatJoiningColumn(relation.joiningColumn.columnName, relation.name), | ||
relation.orphanRemoval ? 'removeOrphan()' : undefined, | ||
]; | ||
}, | ||
visitManyHasManyOwning: ({ entity, relation }) => { | ||
const columnNames = this.conventions.getJoiningTableColumnNames(entity.name, relation.name, relation.target, relation.inversedBy); | ||
const defaultJoiningTable = this.conventions.getJoiningTableName(entity.name, relation.name); | ||
const joiningTable = {}; | ||
if (relation.joiningTable.tableName !== defaultJoiningTable) { | ||
joiningTable.tableName = relation.joiningTable.tableName; | ||
} | ||
if (!relation.joiningTable.eventLog.enabled) { | ||
joiningTable.eventLog = { enabled: false }; | ||
} | ||
if (columnNames[0] !== relation.joiningTable.joiningColumn.columnName || relation.joiningTable.joiningColumn.onDelete !== schema_1.Model.OnDelete.cascade) { | ||
joiningTable.joiningColumn = { | ||
columnName: relation.joiningTable.joiningColumn.columnName, | ||
onDelete: relation.joiningTable.joiningColumn.onDelete, | ||
}; | ||
} | ||
if (columnNames[1] !== relation.joiningTable.inverseJoiningColumn.columnName || relation.joiningTable.inverseJoiningColumn.onDelete !== schema_1.Model.OnDelete.cascade) { | ||
joiningTable.inverseJoiningColumn = { | ||
columnName: relation.joiningTable.inverseJoiningColumn.columnName, | ||
onDelete: relation.joiningTable.inverseJoiningColumn.onDelete, | ||
}; | ||
} | ||
return [ | ||
formatRelationFactory('manyHasMany', relation), | ||
...formatOrderBy(relation.orderBy), | ||
Object.keys(joiningTable).length > 0 ? `joiningTable(${this.formatLiteral(joiningTable)})` : undefined, | ||
]; | ||
}, | ||
visitManyHasManyInverse: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('manyHasManyInverse', relation), | ||
...formatOrderBy(relation.orderBy), | ||
]; | ||
}, | ||
}); | ||
const definitionCode = definition.filter(it => !!it).join('.'); | ||
if (field.name === 'id' && definitionCode === 'uuidColumn().notNull()') { | ||
return undefined; | ||
} | ||
return `\t${this.formatIdentifier(field.name)} = def.${definitionCode}`; | ||
} | ||
generateColumn({ entity, column }) { | ||
let parts = []; | ||
if (column.type === schema_1.Model.ColumnType.Enum) { | ||
parts.push(`enumColumn(${column.columnType})`); | ||
} | ||
else { | ||
parts.push(`${ColumnToMethodMapping[column.type]}()`); | ||
const defaultColumnType = (0, model_1.resolveDefaultColumnType)(column.type); | ||
if (defaultColumnType !== column.columnType) { | ||
parts.push(`columnType(${this.formatLiteral(column.columnType)})`); | ||
} | ||
} | ||
const defaultColumnName = this.conventions.getColumnName(column.name); | ||
if (defaultColumnName !== column.columnName) { | ||
parts.push(`columnName(${this.formatLiteral(column.columnName)})`); | ||
} | ||
if (!column.nullable) { | ||
parts.push('notNull()'); | ||
} | ||
if (column.default !== undefined) { | ||
parts.push(`default(${this.formatLiteral(column.default)})`); | ||
} | ||
if (column.typeAlias) { | ||
parts.push(`typeAlias(${this.formatLiteral(column.typeAlias)})`); | ||
} | ||
// todo: sequence | ||
// todo: maybe single column unique() | ||
return parts; | ||
} | ||
formatIdentifier(id) { | ||
// todo: validate | ||
return id; | ||
} | ||
formatLiteral(value) { | ||
if (value === undefined || value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'function') { | ||
return String(value); | ||
} | ||
if (typeof value === 'bigint') { | ||
return value.toString(10) + 'n'; | ||
} | ||
if (typeof value === 'string') { | ||
return `'${value.replaceAll(/'/g, '\\\'')}'`; | ||
} | ||
if (Array.isArray(value)) { | ||
return `[${value.map(it => this.formatLiteral(it)).join(', ')}]`; | ||
} | ||
return `{${Object.entries(value).map(([key, value]) => { | ||
const formattedKey = this.isSimpleIdentifier(key) ? key : `[${this.formatLiteral(key)}]`; | ||
return `${formattedKey}: ${this.formatLiteral(value)}`; | ||
}).join(', ')}`; | ||
} | ||
isValidIdentifier(identifier) { | ||
if (!this.isSimpleIdentifier(identifier)) { | ||
return false; | ||
} | ||
return !TsDefinitionGenerator.reservedWords.has(identifier); | ||
} | ||
isSimpleIdentifier(identifier) { | ||
return !!identifier.match(/^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/); | ||
} | ||
} | ||
exports.TsDefinitionGenerator = TsDefinitionGenerator; | ||
TsDefinitionGenerator.reservedWords = new Set(['do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof']); | ||
const ColumnToMethodMapping = { | ||
[schema_1.Model.ColumnType.Bool]: 'boolColumn', | ||
[schema_1.Model.ColumnType.Date]: 'dateColumn', | ||
[schema_1.Model.ColumnType.DateTime]: 'dateTimeColumn', | ||
[schema_1.Model.ColumnType.Json]: 'jsonColumn', | ||
[schema_1.Model.ColumnType.Double]: 'doubleColumn', | ||
[schema_1.Model.ColumnType.Uuid]: 'uuidColumn', | ||
[schema_1.Model.ColumnType.Int]: 'intColumn', | ||
[schema_1.Model.ColumnType.String]: 'stringColumn', | ||
}; | ||
//# sourceMappingURL=TsDefinitionGenerator.js.map |
@@ -137,3 +137,3 @@ "use strict"; | ||
if (targetField.inversedBy !== field.name) { | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`); | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.inversedBy} given`); | ||
} | ||
@@ -176,3 +176,3 @@ if (field.type === schema_1.Model.RelationType.OneHasOne && targetField.type !== schema_1.Model.RelationType.OneHasOne) { | ||
if (targetField.ownedBy !== field.name) { | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`); | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.ownedBy} given`); | ||
} | ||
@@ -179,0 +179,0 @@ if (field.type === schema_1.Model.RelationType.OneHasOne && targetField.type !== schema_1.Model.RelationType.OneHasOne) { |
@@ -32,5 +32,6 @@ "use strict"; | ||
const enum_ = __importStar(require("./schemas/enum")); | ||
const acl = __importStar(require("./schemas/acl")); | ||
const promises_1 = require("fs/promises"); | ||
const path_1 = require("path"); | ||
const src_1 = require("../../../src"); | ||
const DefinitionCodeGenerator_1 = require("../../../src/definition-generator/DefinitionCodeGenerator"); | ||
const tests = [ | ||
@@ -41,2 +42,3 @@ ['basic', basic], | ||
['enum', enum_], | ||
['acl', acl], | ||
]; | ||
@@ -46,5 +48,5 @@ for (const [name, def] of tests) { | ||
const schema = (0, schema_definition_1.createSchema)(def); | ||
const generator = new src_1.TsDefinitionGenerator(schema); | ||
const generator = new DefinitionCodeGenerator_1.DefinitionCodeGenerator(); | ||
const content = await (0, promises_1.readFile)((0, path_1.join)(__dirname, `schemas/${name}.ts`), 'utf-8'); | ||
const generated = generator.generate(); | ||
const generated = generator.generate(schema); | ||
try { | ||
@@ -51,0 +53,0 @@ (0, vitest_1.expect)(generated).toBe(content); |
{ | ||
"name": "@contember/schema-utils", | ||
"version": "1.3.0-alpha.6", | ||
"version": "1.3.0-alpha.7", | ||
"license": "Apache-2.0", | ||
@@ -15,8 +15,8 @@ "main": "dist/src/index.js", | ||
"dependencies": { | ||
"@contember/schema": "^1.3.0-alpha.6", | ||
"@contember/typesafe": "^1.3.0-alpha.6", | ||
"@contember/schema": "^1.3.0-alpha.7", | ||
"@contember/typesafe": "^1.3.0-alpha.7", | ||
"crypto-js": "^4.1.1" | ||
}, | ||
"devDependencies": { | ||
"@contember/schema-definition": "^1.3.0-alpha.6", | ||
"@contember/schema-definition": "^1.3.0-alpha.7", | ||
"@types/crypto-js": "^4.1.1", | ||
@@ -23,0 +23,0 @@ "@types/node": "^18" |
export * from './TsDefinitionGenerator' | ||
export * from './AclDefinitionCodeGenerator' | ||
export * from './DefinitionCodeGenerator' | ||
export * from './DefinitionNamingConventions' |
@@ -1,14 +0,9 @@ | ||
import { Model, Schema, Writable } from '@contember/schema' | ||
import { | ||
acceptFieldVisitor, | ||
DefaultNamingConventions, | ||
isInverseRelation, | ||
NamingConventions, | ||
NamingHelper, | ||
resolveDefaultColumnType, | ||
} from '../model' | ||
import { Schema } from '@contember/schema' | ||
import { DefaultNamingConventions, NamingConventions } from '../model' | ||
import { DefinitionCodeGenerator } from './DefinitionCodeGenerator' | ||
/** | ||
* @deprecated use {@link DefinitionCodeGenerator} | ||
*/ | ||
export class TsDefinitionGenerator { | ||
private static reservedWords = new Set(['do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof']) | ||
constructor( | ||
@@ -21,256 +16,5 @@ private readonly schema: Schema, | ||
public generate() { | ||
const enums = Object.entries(this.schema.model.enums).map(([name, values]) => this.generateEnum({ | ||
name, | ||
values, | ||
})).join('') | ||
const entities = Object.values(this.schema.model.entities).map(entity => this.generateEntity({ entity })).join('') | ||
return `import { SchemaDefinition as def } from '@contember/schema-definition' | ||
${enums}${entities}` | ||
const generator = new DefinitionCodeGenerator(this.conventions) | ||
return generator.generate(this.schema) | ||
} | ||
private generateEnum({ name, values }: { name: string; values: readonly string[] }): string { | ||
return `\nexport const ${this.formatIdentifier(name)} = def.createEnum(${values.map(it => this.formatLiteral(it)).join(', ')})\n` | ||
} | ||
public generateEntity({ entity }: { entity: Model.Entity }): string { | ||
const decorators = [ | ||
...Object.values(entity.unique).map(constraint => this.generateUniqueConstraint({ entity, constraint })), | ||
...Object.values(entity.indexes).map(index => this.generateIndex({ entity, index })), | ||
this.generateView({ entity }), | ||
].filter(it => !!it).map(it => `${it}\n`).join('') | ||
return `\n${decorators}export class ${this.formatIdentifier(entity.name)} { | ||
${Object.values(entity.fields).map(field => this.generateField({ field, entity })).filter(it => !!it).join('\n')} | ||
}\n` | ||
} | ||
private generateUniqueConstraint({ entity, constraint }: { entity: Model.Entity; constraint: Model.UniqueConstraint }): string { | ||
const defaultName = NamingHelper.createUniqueConstraintName(entity.name, constraint.fields) | ||
if (defaultName === constraint.name) { | ||
const fieldsList = `${constraint.fields.map(it => this.formatLiteral(it)).join(', ')}` | ||
return `@def.Unique(${fieldsList})` | ||
} | ||
return `@def.Unique(${this.formatLiteral(constraint)})` | ||
} | ||
private generateIndex({ entity, index }: { entity: Model.Entity; index: Model.Index }): string { | ||
const defaultName = NamingHelper.createIndexName(entity.name, index.fields) | ||
if (defaultName === index.name) { | ||
const fieldsList = `${index.fields.map(it => this.formatLiteral(it)).join(', ')}` | ||
return `@def.Index(${fieldsList})` | ||
} | ||
return `@def.Index(${this.formatLiteral(index)})` | ||
} | ||
private generateView({ entity }: { entity: Model.Entity }): string | undefined { | ||
if (!entity.view) { | ||
return undefined | ||
} | ||
const dependenciesExpr = (entity.view.dependencies?.length ?? 0) > 0 | ||
? `, {\n\tdependencies: () => [${entity.view.dependencies?.map(it => this.formatIdentifier(it))}]\n}` | ||
: '' | ||
return `@def.View(\`${entity.view.sql}\`${dependenciesExpr})` | ||
} | ||
public generateField({ entity, field }: { entity: Model.Entity; field: Model.AnyField }): string | undefined { | ||
const formatRelationFactory = (method: string, relation: Model.AnyRelation) => { | ||
const otherSide = isInverseRelation(relation) ? relation.ownedBy : relation.inversedBy | ||
const otherSideFormatted = otherSide ? `, ${this.formatLiteral(otherSide)}` : '' | ||
return `${method}(${this.formatIdentifier(relation.target)}${otherSideFormatted})` | ||
} | ||
const formatEnumRef = (enumName: string, enumValues: Record<string, string>, providedValue?: string, defaultValue?: string): string | undefined => { | ||
if (!providedValue || providedValue === defaultValue) { | ||
return undefined | ||
} | ||
const enumValueKey = Object.entries(Model.OrderDirection).find(dir => dir[1] === providedValue)?.[0] | ||
if (!enumValueKey) { | ||
throw new Error(`Value ${providedValue} is not defined in enum ${enumName}`) | ||
} | ||
return `${enumName}.${enumValueKey}` | ||
} | ||
const formatOrderBy = (orderBy?: readonly Model.OrderBy[]) => { | ||
return orderBy?.map(it => { | ||
const enumExpr = formatEnumRef(`Model.OrderDirection`, Model.OrderDirection, it.direction, Model.OrderDirection.asc) | ||
return `orderBy(${this.formatLiteral(it.path)}${enumExpr ? `, ${enumExpr}` : ''})` | ||
}) ?? [] | ||
} | ||
const formatOnDelete = (onDelete?: Model.OnDelete): string | undefined => { | ||
if (onDelete === Model.OnDelete.cascade) { | ||
return 'cascadeOnDelete()' | ||
} | ||
if (onDelete === Model.OnDelete.setNull) { | ||
return 'setNullOnDelete()' | ||
} | ||
return undefined | ||
} | ||
const formatJoiningColumn = (joiningColumnName: string, fieldName: string): string | undefined => { | ||
const defaultJoiningColumn = this.conventions.getJoiningColumnName(fieldName) | ||
if (defaultJoiningColumn === joiningColumnName) { | ||
return undefined | ||
} | ||
return `joiningColumn(${this.formatLiteral(joiningColumnName)})` | ||
} | ||
const definition = acceptFieldVisitor<(string | undefined)[]>(this.schema.model, entity, field, { | ||
visitColumn: ctx => { | ||
return this.generateColumn(ctx) | ||
}, | ||
visitOneHasMany: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('oneHasMany', relation), | ||
...formatOrderBy(relation.orderBy), | ||
] | ||
}, | ||
visitManyHasOne: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('manyHasOne', relation), | ||
!relation.nullable ? 'notNull()' : undefined, | ||
formatOnDelete(relation.joiningColumn.onDelete), | ||
formatJoiningColumn(relation.joiningColumn.columnName, relation.name), | ||
] | ||
}, | ||
visitOneHasOneInverse: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('oneHasOneInverse', relation), | ||
!relation.nullable ? 'notNull()' : undefined, | ||
] | ||
}, | ||
visitOneHasOneOwning: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('oneHasOne', relation), | ||
!relation.nullable ? 'notNull()' : undefined, | ||
formatOnDelete(relation.joiningColumn.onDelete), | ||
formatJoiningColumn(relation.joiningColumn.columnName, relation.name), | ||
relation.orphanRemoval ? 'removeOrphan()' : undefined, | ||
] | ||
}, | ||
visitManyHasManyOwning: ({ entity, relation }) => { | ||
const columnNames = this.conventions.getJoiningTableColumnNames( | ||
entity.name, | ||
relation.name, | ||
relation.target, | ||
relation.inversedBy, | ||
) | ||
const defaultJoiningTable = this.conventions.getJoiningTableName(entity.name, relation.name) | ||
const joiningTable: Writable<Partial<Model.JoiningTable>> = {} | ||
if (relation.joiningTable.tableName !== defaultJoiningTable) { | ||
joiningTable.tableName = relation.joiningTable.tableName | ||
} | ||
if (!relation.joiningTable.eventLog.enabled) { | ||
joiningTable.eventLog = { enabled: false } | ||
} | ||
if (columnNames[0] !== relation.joiningTable.joiningColumn.columnName || relation.joiningTable.joiningColumn.onDelete !== Model.OnDelete.cascade) { | ||
joiningTable.joiningColumn = { | ||
columnName: relation.joiningTable.joiningColumn.columnName, | ||
onDelete: relation.joiningTable.joiningColumn.onDelete, | ||
} | ||
} | ||
if (columnNames[1] !== relation.joiningTable.inverseJoiningColumn.columnName || relation.joiningTable.inverseJoiningColumn.onDelete !== Model.OnDelete.cascade) { | ||
joiningTable.inverseJoiningColumn = { | ||
columnName: relation.joiningTable.inverseJoiningColumn.columnName, | ||
onDelete: relation.joiningTable.inverseJoiningColumn.onDelete, | ||
} | ||
} | ||
return [ | ||
formatRelationFactory('manyHasMany', relation), | ||
...formatOrderBy(relation.orderBy), | ||
Object.keys(joiningTable).length > 0 ? `joiningTable(${this.formatLiteral(joiningTable)})` : undefined, | ||
] | ||
}, | ||
visitManyHasManyInverse: ({ relation }) => { | ||
return [ | ||
formatRelationFactory('manyHasManyInverse', relation), | ||
...formatOrderBy(relation.orderBy), | ||
] | ||
}, | ||
}) | ||
const definitionCode = definition.filter(it => !!it).join('.') | ||
if (field.name === 'id' && definitionCode === 'uuidColumn().notNull()') { | ||
return undefined | ||
} | ||
return `\t${this.formatIdentifier(field.name)} = def.${definitionCode}` | ||
} | ||
private generateColumn({ entity, column }: { entity: Model.Entity; column: Model.AnyColumn }): string[] { | ||
let parts: string[] = [] | ||
if (column.type === Model.ColumnType.Enum) { | ||
parts.push(`enumColumn(${column.columnType})`) | ||
} else { | ||
parts.push(`${ColumnToMethodMapping[column.type]}()`) | ||
const defaultColumnType = resolveDefaultColumnType(column.type) | ||
if (defaultColumnType !== column.columnType) { | ||
parts.push(`columnType(${this.formatLiteral(column.columnType)})`) | ||
} | ||
} | ||
const defaultColumnName = this.conventions.getColumnName(column.name) | ||
if (defaultColumnName !== column.columnName) { | ||
parts.push(`columnName(${this.formatLiteral(column.columnName)})`) | ||
} | ||
if (!column.nullable) { | ||
parts.push('notNull()') | ||
} | ||
if (column.default !== undefined) { | ||
parts.push(`default(${this.formatLiteral(column.default)})`) | ||
} | ||
if (column.typeAlias) { | ||
parts.push(`typeAlias(${this.formatLiteral(column.typeAlias)})`) | ||
} | ||
// todo: sequence | ||
// todo: maybe single column unique() | ||
return parts | ||
} | ||
private formatIdentifier(id: string): string { | ||
// todo: validate | ||
return id | ||
} | ||
private formatLiteral(value: any): string { | ||
if (value === undefined || value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'function') { | ||
return String(value) | ||
} | ||
if (typeof value === 'bigint') { | ||
return value.toString(10) + 'n' | ||
} | ||
if (typeof value === 'string') { | ||
return `'${value.replaceAll(/'/g, '\\\'')}'` | ||
} | ||
if (Array.isArray(value)) { | ||
return `[${value.map(it => this.formatLiteral(it)).join(', ')}]` | ||
} | ||
return `{${Object.entries(value).map(([key, value]) => { | ||
const formattedKey = this.isSimpleIdentifier(key) ? key : `[${this.formatLiteral(key)}]` | ||
return `${formattedKey}: ${this.formatLiteral(value)}` | ||
}).join(', ')}` | ||
} | ||
private isValidIdentifier(identifier: string): boolean { | ||
if (!this.isSimpleIdentifier(identifier)) { | ||
return false | ||
} | ||
return !TsDefinitionGenerator.reservedWords.has(identifier) | ||
} | ||
private isSimpleIdentifier(identifier: string): boolean { | ||
return !!identifier.match(/^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/) | ||
} | ||
} | ||
const ColumnToMethodMapping: { | ||
[K in Exclude<Model.ColumnType, Model.ColumnType.Enum>]: string | ||
} = { | ||
[Model.ColumnType.Bool]: 'boolColumn', | ||
[Model.ColumnType.Date]: 'dateColumn', | ||
[Model.ColumnType.DateTime]: 'dateTimeColumn', | ||
[Model.ColumnType.Json]: 'jsonColumn', | ||
[Model.ColumnType.Double]: 'doubleColumn', | ||
[Model.ColumnType.Uuid]: 'uuidColumn', | ||
[Model.ColumnType.Int]: 'intColumn', | ||
[Model.ColumnType.String]: 'stringColumn', | ||
} |
@@ -10,3 +10,3 @@ import { Model } from '@contember/schema' | ||
export class ModelValidator { | ||
constructor(private readonly model: Model.Schema) {} | ||
constructor(private readonly model: Model.Schema) { } | ||
@@ -83,3 +83,3 @@ public validate(): ValidationError[] { | ||
private validateRelation(partialEntity: Model.Entity, field: Model.AnyRelation, errors: ErrorBuilder): void { | ||
private validateRelation(partialEntity: Model.Entity, field: Model.AnyRelation, errors: ErrorBuilder): void { | ||
const entityName = partialEntity.name | ||
@@ -153,3 +153,3 @@ const targetEntityName = field.target | ||
if (targetField.inversedBy !== field.name) { | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`) | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.inversedBy} given`) | ||
} | ||
@@ -191,3 +191,3 @@ if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) { | ||
if (targetField.ownedBy !== field.name) { | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`) | ||
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.ownedBy} given`) | ||
} | ||
@@ -266,7 +266,7 @@ if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) { | ||
}, | ||
visitManyHasManyInverse: () => {}, | ||
visitOneHasMany: () => {}, | ||
visitOneHasOneInverse: () => {}, | ||
visitOneHasOneOwning: () => {}, | ||
visitManyHasOne: () => {}, | ||
visitManyHasManyInverse: () => { }, | ||
visitOneHasMany: () => { }, | ||
visitOneHasOneInverse: () => { }, | ||
visitOneHasOneOwning: () => { }, | ||
visitManyHasOne: () => { }, | ||
}) | ||
@@ -273,0 +273,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { SchemaDefinition as def } from '@contember/schema-definition' | ||
import { SchemaDefinition as def, AclDefinition as acl } from '@contember/schema-definition' | ||
@@ -3,0 +3,0 @@ export class Article { |
@@ -1,2 +0,2 @@ | ||
import { SchemaDefinition as def } from '@contember/schema-definition' | ||
import { SchemaDefinition as def, AclDefinition as acl } from '@contember/schema-definition' | ||
@@ -3,0 +3,0 @@ export const articleState = def.createEnum('draft', 'published') |
@@ -1,2 +0,2 @@ | ||
import { SchemaDefinition as def } from '@contember/schema-definition' | ||
import { SchemaDefinition as def, AclDefinition as acl } from '@contember/schema-definition' | ||
@@ -3,0 +3,0 @@ export class Article { |
@@ -1,2 +0,2 @@ | ||
import { SchemaDefinition as def } from '@contember/schema-definition' | ||
import { SchemaDefinition as def, AclDefinition as acl } from '@contember/schema-definition' | ||
@@ -3,0 +3,0 @@ @def.Unique('title') |
import { createSchema } from '@contember/schema-definition' | ||
import { test, expect } from 'vitest' | ||
import { expect, test } from 'vitest' | ||
import * as basic from './schemas/basic' | ||
@@ -7,5 +7,6 @@ import * as relations from './schemas/relations' | ||
import * as enum_ from './schemas/enum' | ||
import * as acl from './schemas/acl' | ||
import { readFile, writeFile } from 'fs/promises' | ||
import { join } from 'path' | ||
import { TsDefinitionGenerator } from '../../../src' | ||
import { DefinitionCodeGenerator } from '../../../src/definition-generator/DefinitionCodeGenerator' | ||
@@ -18,2 +19,3 @@ | ||
['enum', enum_], | ||
['acl', acl], | ||
] as const | ||
@@ -23,5 +25,5 @@ for (const [name, def] of tests) { | ||
const schema = createSchema(def) | ||
const generator = new TsDefinitionGenerator(schema) | ||
const generator = new DefinitionCodeGenerator() | ||
const content = await readFile(join(__dirname, `schemas/${name}.ts`), 'utf-8') | ||
const generated = generator.generate() | ||
const generated = generator.generate(schema) | ||
try { | ||
@@ -28,0 +30,0 @@ expect(generated).toBe(content) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
579675
280
8368
49