UNPKG

@directus/schema-builder

Version:

Directus SchemaBuilder for mocking/constructing a database schema based on code.

732 lines (724 loc) 21.4 kB
// src/builder.ts import { ok as assert4 } from "assert/strict"; // src/collection.ts import { ok as assert3 } from "assert/strict"; // src/defaults.ts var COLLECTION_DEFAULTS = { singleton: false, sortField: null, note: null, accountability: "all" }; var FIELD_DEFAULTS = { defaultValue: null, nullable: true, generated: false, precision: null, scale: null, special: [], note: null, validation: null, alias: false }; var INTEGER_FIELD = { ...FIELD_DEFAULTS, type: "integer", dbType: "integer" }; var ID_FIELD = { ...INTEGER_FIELD, defaultValue: "AUTO_INCREMENT", nullable: false }; var JSON_FIELD = { ...FIELD_DEFAULTS, type: "json", dbType: "json" }; var DATE_FIELD = { ...FIELD_DEFAULTS, type: "date", dbType: "date" }; var TIME_FIELD = { ...FIELD_DEFAULTS, type: "time", dbType: "time without time zone" }; var DATE_TIME_FIELD = { ...FIELD_DEFAULTS, type: "dateTime", dbType: "timestamp without time zone" }; var TIMESTAMP_FIELD = { ...FIELD_DEFAULTS, type: "timestamp", dbType: "timestamp with time zone" }; var HASH_FIELD = { ...FIELD_DEFAULTS, type: "hash", dbType: "character varying", special: ["hash"] }; var CSV_FIELD = { ...FIELD_DEFAULTS, type: "csv", dbType: "text", special: ["cast-csv"] }; var BIG_INTEGER_FIELD = { ...FIELD_DEFAULTS, type: "bigInteger", dbType: "bigint" }; var FLOAT_FIELD = { ...FIELD_DEFAULTS, type: "float", dbType: "real" }; var DECIMAL_FIELD = { ...FIELD_DEFAULTS, type: "decimal", dbType: "numeric" }; var STRING_FIELD = { ...FIELD_DEFAULTS, type: "string", dbType: "character varying" }; var UUID_FIELD = { ...FIELD_DEFAULTS, type: "uuid", dbType: "uuid", special: ["uuid"] }; var TEXT_FIELD = { ...FIELD_DEFAULTS, type: "text", dbType: "text" }; var BOOLEAN_FIELD = { ...FIELD_DEFAULTS, type: "boolean", dbType: "boolean", special: ["cast-boolean"] }; var RELATION_DEFAULTS = { meta: { sort_field: null, one_deselect_action: "nullify" }, schema: { foreign_key_schema: "public", on_update: "NO ACTION", on_delete: "SET NULL" } }; // src/field.ts import { ok as assert2 } from "assert/strict"; // src/relation.ts import { ok as assert } from "assert/strict"; import { merge } from "lodash-es"; var RelationBuilder = class { _schemaBuilder; _data; constructor(collection, field, schema) { this._data = { collection, field, _kind: "initial" }; this._schemaBuilder = schema; } o2m(related_collection, related_field) { assert(this._data._kind === "initial", "Relation is already configured"); merge(this._data, RELATION_DEFAULTS, { collection: related_collection, field: related_field, related_collection: this._data.collection, meta: { many_collection: related_collection, many_field: related_field, one_collection: this._data.collection, one_field: this._data.field, one_collection_field: null, one_allowed_collections: null, id: this._schemaBuilder?.next_relation_index() ?? 0, junction_field: null }, schema: { constraint_name: `${this._data.collection}_${this._data.field}_foreign`, table: this._data.collection, column: this._data.field, foreign_key_table: related_collection }, _kind: "finished", _type: "o2m" }); return this; } m2o(related_collection, related_field) { assert(this._data._kind === "initial", "Relation is already configured"); merge(this._data, RELATION_DEFAULTS, { collection: this._data.collection, field: this._data.field, related_collection, meta: { many_collection: this._data.collection, many_field: this._data.field, one_collection: related_collection, one_field: related_field ?? null, one_collection_field: null, one_allowed_collections: null, id: this._schemaBuilder?.next_relation_index() ?? 0, junction_field: null }, schema: { constraint_name: `${this._data.collection}_${this._data.field}_foreign`, table: this._data.collection, column: this._data.field, foreign_key_table: related_collection }, _kind: "finished", _type: "m2o" }); return this; } a2o(related_collections) { assert(this._data._kind === "initial", "Relation is already configured"); merge(this._data, RELATION_DEFAULTS, { collection: this._data.collection, field: this._data.field, related_collection: null, meta: { many_collection: this._data.collection, many_field: this._data.field, one_collection: null, one_field: null, one_collection_field: "collection", one_allowed_collections: related_collections, id: this._schemaBuilder?.next_relation_index() ?? 0, junction_field: null }, schema: null, _kind: "finished", _type: "a2o" }); return this; } options(options) { assert(this._data._kind === "finished", "Relation is not yet configured"); merge(this._data, options); return this; } build(schema) { assert(this._data._kind === "finished", "Relation type is not configured"); if (this._data._type === "m2o" || this._data._type === "o2m") { if (this._data.related_collection && this._data.related_collection in schema.collections === false) { const collection2 = new CollectionBuilder(this._data.related_collection); collection2.field("id").id(); schema.collections[this._data.related_collection] = collection2.build(schema); } } if (this._data.collection && this._data.collection in schema.collections === false) { const collection2 = new CollectionBuilder(this._data.collection); collection2.field("id").id(); schema.collections[this._data.collection] = collection2.build(schema); } const collection = schema.collections[this._data.collection]; if (this._data.field && this._data.field in collection.fields === false) { const key_type = collection.fields[collection.primary].type; assert( key_type === "integer" || key_type === "string", `Cannot generate related field for primary key type ${key_type}` ); const field = new FieldBuilder(this._data.field)[key_type](); collection.fields[this._data.field] = field.build(schema); } if (this._data._type === "a2o") { const collection_field = this._data.meta?.one_collection_field; if (collection_field && collection_field in collection.fields === false) { const field = new FieldBuilder(collection_field).string(); collection.fields[collection_field] = field.build(schema); } for (const collection_name of this._data.meta?.one_allowed_collections ?? []) { if (collection_name in schema.collections) continue; const collection2 = new CollectionBuilder(collection_name); collection2.field("id").id(); schema.collections[collection_name] = collection2.build(schema); } } const { _kind, _type, ...relation } = this._data; return relation; } }; // src/field.ts var FieldBuilder = class { _schema; _collection; _data; constructor(name, schema, collection) { this._data = { field: name, _kind: "initial" }; this._schema = schema; this._collection = collection; } /** Shorthand for creating an integer field and marking it as the primary field */ id() { this._data = { field: this._data.field, ...ID_FIELD, _kind: "finished" }; if (this._collection) this.primary(); return this; } options(options) { assert2(this._data._kind !== "initial", "Cannot configure field before specifing a type"); Object.assign(this._data, options); return this; } /** Resets the field to it's initial state of only the name */ overwrite() { this._data = { field: this._data.field, _kind: "initial" }; return this; } /** Marks the field as the primary field of the collection */ primary() { assert2(this._collection, "Can only set to primary on a collection"); assert2( "primary" in this._collection._data === false, `The primary key is already set on the collection ${this._collection.get_name()}` ); this._collection._data = { primary: this._data.field, ...this._collection._data }; return this; } /** Marks the field as the sort_field of the collection */ sort() { assert2(this._collection, "Can only set to sort on a collection"); assert2(this._collection._data.sortField === null, "Can only set a sort field once"); this._collection._data = { ...this._collection._data, sortField: this._data.field }; } boolean() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...BOOLEAN_FIELD, _kind: "finished" }; return this; } bigInteger() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...BIG_INTEGER_FIELD, _kind: "finished" }; return this; } date() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...DATE_FIELD, _kind: "finished" }; return this; } dateTime() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...DATE_TIME_FIELD, _kind: "finished" }; return this; } decimal() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...DECIMAL_FIELD, _kind: "finished" }; return this; } float() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...FLOAT_FIELD, _kind: "finished" }; return this; } integer() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...INTEGER_FIELD, _kind: "finished" }; return this; } json() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...JSON_FIELD, _kind: "finished" }; return this; } string() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...STRING_FIELD, _kind: "finished" }; return this; } text() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...TEXT_FIELD, _kind: "finished" }; return this; } time() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...TIME_FIELD, _kind: "finished" }; return this; } timestamp() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...TIMESTAMP_FIELD, _kind: "finished" }; return this; } uuid() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...UUID_FIELD, _kind: "finished" }; return this; } hash() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...HASH_FIELD, _kind: "finished" }; return this; } csv() { assert2(this._data._kind === "initial", "Field type was already set"); this._data = { field: this._data.field, ...CSV_FIELD, _kind: "finished" }; return this; } m2a(related_collections, relation_callback) { assert2(this._data._kind === "initial", "Field type was already set"); assert2(this._schema && this._collection, "Field needs to be part of a schema"); this._data = { field: this._data.field, ...FIELD_DEFAULTS, type: "alias", dbType: null, special: ["m2a"], _kind: "finished" }; const junction_name = `${this._collection.get_name()}_builder`; let o2m_relation = new RelationBuilder(this._collection.get_name(), this.get_name()).o2m(junction_name, `${this._collection.get_name()}_id`).options({ meta: { junction_field: `item` } }); let a2o_relation = new RelationBuilder(junction_name, "item").a2o(related_collections).options({ meta: { junction_field: `${this._collection.get_name()}_id` } }); if (relation_callback) { const new_relations = relation_callback({ o2m_relation, a2o_relation }); if (new_relations) { o2m_relation = new_relations.o2m_relation; a2o_relation = new_relations.a2o_relation; } } this._schema._relations.push(o2m_relation); this._schema._relations.push(a2o_relation); return this; } m2m(related_collection, relation_callback) { assert2(this._data._kind === "initial", "Field type was already set"); assert2(this._schema && this._collection, "Field needs to be part of a schema"); this._data = { field: this._data.field, ...FIELD_DEFAULTS, type: "alias", dbType: null, special: ["m2m"], _kind: "finished" }; const junction_name = `${this._collection.get_name()}_${related_collection}_junction`; let o2m_relation = new RelationBuilder(this._collection.get_name(), this.get_name()).o2m(junction_name, `${this._collection.get_name()}_id`).options({ meta: { junction_field: `${related_collection}_id` } }); let m2o_relation = new RelationBuilder(junction_name, `${related_collection}_id`).m2o(related_collection).options({ meta: { junction_field: `${this._collection.get_name()}_id` } }); if (relation_callback) { const new_relations = relation_callback({ o2m_relation, m2o_relation }); if (new_relations) { o2m_relation = new_relations.o2m_relation; m2o_relation = new_relations.m2o_relation; } } this._schema._relations.push(o2m_relation); this._schema._relations.push(m2o_relation); return this; } translations(language_collection = "languages", relation_callback) { assert2(this._data._kind === "initial", "Field type was already set"); assert2(this._schema && this._collection, "Field needs to be part of a schema"); this._data = { field: this._data.field, ...FIELD_DEFAULTS, type: "alias", dbType: null, special: ["translations"], _kind: "finished" }; this._schema.collection(language_collection, (c) => { c.field("code").string().primary(); c.field("name").string(); c.field("direction").string().options({ defaultValue: "ltr" }); }); const junction_name = `${this._collection.get_name()}_translations`; let o2m_relation = new RelationBuilder(this._collection.get_name(), this.get_name()).o2m(junction_name, `${this._collection.get_name()}_id`).options({ meta: { junction_field: `${language_collection}_code` } }); let m2o_relation = new RelationBuilder(junction_name, `${language_collection}_code`).m2o(language_collection).options({ meta: { junction_field: `${this._collection.get_name()}_id` } }); if (relation_callback) { const new_relations = relation_callback({ o2m_relation, m2o_relation }); if (new_relations) { o2m_relation = new_relations.o2m_relation; m2o_relation = new_relations.m2o_relation; } } this._schema._relations.push(o2m_relation); this._schema._relations.push(m2o_relation); return this; } o2m(related_collection, related_field, relation_callback) { assert2(this._data._kind === "initial", "Field type was already set"); assert2(this._schema && this._collection, "Field needs to be part of a schema"); this._data = { field: this._data.field, ...FIELD_DEFAULTS, type: "alias", dbType: null, special: ["o2m"], _kind: "finished" }; let relation = new RelationBuilder(this._collection.get_name(), this.get_name()).o2m( related_collection, related_field ); if (relation_callback) { const new_relation = relation_callback(relation); if (new_relation) { relation = new_relation; } } this._schema._relations.push(relation); return this; } m2o(related_collection, related_field, relation_callback) { assert2(this._data._kind === "initial", "Field type was already set"); assert2(this._schema && this._collection, "Field needs to be part of a schema"); this._data = { field: this._data.field, ...FIELD_DEFAULTS, type: "integer", dbType: "integer", special: ["m2o"], _kind: "finished" }; let relation = new RelationBuilder(this._collection.get_name(), this.get_name()).m2o( related_collection, related_field ); if (relation_callback) { const new_relation = relation_callback(relation); if (new_relation) { relation = new_relation; } } this._schema._relations.push(relation); return this; } a2o(related_collections, relation_callback) { assert2(this._data._kind === "initial", "Field type was already set"); assert2(this._schema && this._collection, "Field needs to be part of a schema"); this._data = { field: this._data.field, ...FIELD_DEFAULTS, type: "integer", dbType: "integer", special: [], _kind: "finished" }; let relation = new RelationBuilder(this._collection.get_name(), this.get_name()).a2o(related_collections); if (relation_callback) { const new_relation = relation_callback(relation); if (new_relation) { relation = new_relation; } } this._schema._relations.push(relation); return this; } get_name() { return this._data.field; } build(_schema) { assert2(this._data._kind === "finished", "The collection needs at least 1 field configured"); const { _kind, ...field } = this._data; return field; } }; // src/collection.ts var CollectionBuilder = class { _schemaBuilder; _data; _fields = []; constructor(name, schema) { this._data = { collection: name, ...COLLECTION_DEFAULTS }; this._schemaBuilder = schema; } field(name) { const existingField = this._fields.find((fieldBuilder) => fieldBuilder.get_name() === name); if (existingField) { return existingField; } const field = new FieldBuilder(name, this._schemaBuilder, this); this._fields.push(field); return field; } get_name() { return this._data.collection; } build(schema) { assert3("primary" in this._data, `The collection ${this.get_name()} needs a primary key`); const fields = {}; for (const fieldBuilder of this._fields) { const field = fieldBuilder.build(schema); assert3(field.field in fields === false, `Field ${field.field} already exists`); fields[field.field] = field; } const collection = { ...this._data, fields }; return collection; } }; // src/builder.ts var SchemaBuilder3 = class { _collections = []; _relations = []; _last_collection_configured = true; _relation_counter = 0; collection(name, callback) { const existing_index = this._collections.findIndex((collectionBuilder) => collectionBuilder.get_name() === name); if (existing_index !== -1) { callback(this._collections[existing_index]); this._last_collection_configured = false; return this; } const collection = new CollectionBuilder(name, this); callback(collection); this._collections.push(collection); this._last_collection_configured = false; return this; } options(options) { assert4(this._collections.length > 0, "You need at least 1 collection to configure it's options"); assert4(this._last_collection_configured === false, "You can only configure a collection once"); const lastCollection = this._collections.at(-1); Object.assign(lastCollection._data, options); this._last_collection_configured = true; } next_relation_index() { return this._relation_counter++; } build() { const schema = { collections: {}, relations: [] }; for (const collectionBuilder of this._collections) { const collection = collectionBuilder.build(schema); assert4( collection.collection in schema.collections === false, `Collection ${collection.collection} already exists` ); schema.collections[collection.collection] = collection; } for (const relationBuilder of this._relations) { const relation = relationBuilder.build(schema); schema.relations.push(relation); } return schema; } }; export { CollectionBuilder, FieldBuilder, RelationBuilder, SchemaBuilder3 as SchemaBuilder };