feat: improve database

This commit is contained in:
chenos 2021-09-18 00:23:21 +08:00
parent 17db8b8afb
commit a51c5058fb
19 changed files with 1084 additions and 0 deletions

View File

@ -0,0 +1,21 @@
{
"name": "@nocobase/collections",
"version": "0.4.0-alpha.7",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "MIT",
"scripts": {},
"dependencies": {
"bcrypt": "^5.0.0",
"deepmerge": "^4.2.2",
"glob": "^7.1.6",
"sequelize": "^6.3.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nocobase/nocobase.git",
"directory": "packages/database"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -0,0 +1,40 @@
import merge from 'deepmerge';
import { Database, DatabaseOptions } from '../database';
export function generatePrefixByPath() {
const { id } = require.main;
const key = id
.replace(`${process.env.PWD}/packages`, '')
.replace(/src\/__tests__/g, '')
.replace('.test.ts', '')
.replace(/[^\w]/g, '_')
.replace(/_+/g, '_');
return key
}
export function getConfig(config = {}, options?: any): DatabaseOptions {
return merge({
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
logging: process.env.DB_LOG_SQL === 'on',
sync: {
force: true,
alter: {
drop: true,
},
},
hooks: {
beforeDefine(model, options) {
options.tableName = `${generatePrefixByPath()}_${options.tableName || options.modelName || options.name.plural}`;
},
},
}, config || {}, options) as any;
};
export function mockDatabase(options?: DatabaseOptions): Database {
return new Database(getConfig(options));
}

View File

@ -0,0 +1,151 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
describe('belongs to field', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('association undefined', async () => {
const Comment = db.collection({
name: 'comments',
schema: [{ type: 'belongsTo', name: 'post' }],
});
expect(Comment.model.associations['post']).toBeUndefined();
});
it('association defined', async () => {
const Comment = db.collection({
name: 'comments',
schema: [
{ type: 'string', name: 'content' },
{ type: 'belongsTo', name: 'post' },
],
});
expect(Comment.model.associations.post).toBeUndefined();
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'title' },
],
});
const association = Comment.model.associations.post;
expect(Comment.model.associations.post).toBeDefined();
expect(association.foreignKey).toBe('postId');
// @ts-ignore
expect(association.targetKey).toBe('id');
expect(Comment.model.rawAttributes.postId).toBeDefined();
await db.sync();
const comment = await Comment.model.create<any>();
await comment.createPost({
title: 'title222',
});
const post1 = await comment.getPost();
expect(post1.toJSON()).toMatchObject({
title: 'title222',
});
const post = await Post.model.create<any>({
title: 'title111'
});
await comment.setPost(post);
const post2 = await comment.getPost();
expect(post2.toJSON()).toMatchObject({
title: 'title111',
});
});
it('custom targetKey and foreignKey', async () => {
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'key', unique: true },
],
});
const Comment = db.collection({
name: 'comments',
schema: [
{
type: 'belongsTo',
name: 'post',
targetKey: 'key',
foreignKey: 'postKey',
},
],
});
const association = Comment.model.associations.post;
expect(association).toBeDefined();
expect(association.foreignKey).toBe('postKey');
// @ts-ignore
expect(association.targetKey).toBe('key');
expect(Comment.model.rawAttributes['postKey']).toBeDefined();
});
it('custom name and target', async () => {
const Comment = db.collection({
name: 'comments',
schema: [
{ type: 'string', name: 'content' },
{
type: 'belongsTo',
name: 'article',
target: 'posts',
targetKey: 'key',
foreignKey: 'postKey',
},
],
});
expect(Comment.model.associations.article).toBeUndefined();
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'key', unique: true },
],
});
const association = Comment.model.associations.article;
expect(Comment.model.associations.article).toBeDefined();
expect(association.foreignKey).toBe('postKey');
// @ts-ignore
expect(association.targetKey).toBe('key');
expect(Comment.model.rawAttributes.postKey).toBeDefined();
await db.sync();
const comment = await Comment.model.create<any>();
await comment.createArticle({
key: 'title222',
});
const post1 = await comment.getArticle();
expect(post1.toJSON()).toMatchObject({
key: 'title222',
});
const post = await Post.model.create<any>({
key: 'title111'
});
await comment.setArticle(post);
const post2 = await comment.getArticle();
expect(post2.toJSON()).toMatchObject({
key: 'title111',
});
});
it('schema delete', async () => {
const Comment = db.collection({
name: 'comments',
schema: [{ type: 'belongsTo', name: 'post' }],
});
const Post = db.collection({
name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }],
});
// await db.sync();
Comment.schema.delete('post');
expect(Comment.model.associations.post).toBeUndefined();
expect(Comment.model.rawAttributes.postId).toBeDefined();
Post.schema.delete('comments');
expect(Comment.model.rawAttributes.postId).toBeUndefined();
});
});

View File

@ -0,0 +1,42 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
describe('belongs to many field', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('association undefined', async () => {
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'name' },
{ type: 'belongsToMany', name: 'tags' },
],
});
expect(Post.model.associations.tags).toBeUndefined();
expect(db.getCollection('posts_tags')).toBeUndefined();
const Tag = db.collection({
name: 'tags',
schema: [
{ type: 'string', name: 'name' },
],
});
expect(Post.model.associations.tags).toBeDefined();
const Through = db.getCollection('posts_tags');
expect(Through).toBeDefined();
expect(Through.model.rawAttributes['postId']).toBeDefined();
expect(Through.model.rawAttributes['tagId']).toBeDefined();
const PostTag = db.collection({
name: 'posts_tags',
});
expect(PostTag.model.rawAttributes['postId']).toBeDefined();
expect(PostTag.model.rawAttributes['tagId']).toBeDefined();
});
});

View File

@ -0,0 +1,129 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
describe('has many field', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('association undefined', async () => {
const collection = db.collection({
name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }],
});
await db.sync();
expect(collection.model.associations['comments']).toBeUndefined();
});
it('association defined', async () => {
const { model } = db.collection({
name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }],
});
expect(model.associations['comments']).toBeUndefined();
const comments = db.collection({
name: 'comments',
schema: [{ type: 'string', name: 'content' }],
});
const association = model.associations.comments;
expect(association).toBeDefined();
expect(association.foreignKey).toBe('postId');
// @ts-ignore
expect(association.sourceKey).toBe('id');
expect(comments.model.rawAttributes['postId']).toBeDefined();
await db.sync();
const post = await model.create<any>();
await post.createComment({
content: 'content111',
});
const postComments = await post.getComments();
expect(postComments.map((comment) => comment.content)).toEqual([
'content111',
]);
});
it('custom sourceKey and foreignKey', async () => {
const collection = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'key', unique: true },
{
type: 'hasMany',
name: 'comments',
sourceKey: 'key',
foreignKey: 'postKey',
},
],
});
const comments = db.collection({
name: 'comments',
schema: [],
});
const association = collection.model.associations.comments;
expect(association).toBeDefined();
expect(association.foreignKey).toBe('postKey');
// @ts-ignore
expect(association.sourceKey).toBe('key');
expect(comments.model.rawAttributes['postKey']).toBeDefined();
await db.sync();
});
it('custom name and target', async () => {
const collection = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'key', unique: true },
{
type: 'hasMany',
name: 'reviews',
target: 'comments',
sourceKey: 'key',
foreignKey: 'postKey',
},
],
});
db.collection({
name: 'comments',
schema: [{ type: 'string', name: 'content' }],
});
const association = collection.model.associations.reviews;
expect(association).toBeDefined();
expect(association.foreignKey).toBe('postKey');
// @ts-ignore
expect(association.sourceKey).toBe('key');
await db.sync();
const post = await collection.model.create<any>({
key: 'key1',
});
await post.createReview({
content: 'content111',
});
const postComments = await post.getReviews();
expect(postComments.map((comment) => comment.content)).toEqual([
'content111',
]);
});
it('schema delete', async () => {
const Post = db.collection({
name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }],
});
const Comment = db.collection({
name: 'comments',
schema: [{ type: 'belongsTo', name: 'post' }],
});
await db.sync();
Post.schema.delete('comments');
expect(Post.model.associations.comments).toBeUndefined();
expect(Comment.model.rawAttributes.postId).toBeDefined();
Comment.schema.delete('post');
expect(Comment.model.rawAttributes.postId).toBeUndefined();
});
});

View File

@ -0,0 +1,79 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
describe('string field', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('define', async () => {
const Test = db.collection({
name: 'tests',
schema: [
{ type: 'string', name: 'name' },
],
});
await db.sync();
expect(Test.model.rawAttributes['name']).toBeDefined();
const model = await Test.model.create({
name: 'abc',
});
expect(model.toJSON()).toMatchObject({
name: 'abc',
});
});
it('set', async () => {
const Test = db.collection({
name: 'tests',
schema: [
{ type: 'string', name: 'name1' },
],
});
await db.sync();
Test.schema.set('name2', { type: 'string' });
await db.sync();
expect(Test.model.rawAttributes['name1']).toBeDefined();
expect(Test.model.rawAttributes['name2']).toBeDefined();
const model = await Test.model.create({
name1: 'a1',
name2: 'a2',
});
expect(model.toJSON()).toMatchObject({
name1: 'a1',
name2: 'a2',
});
});
it('model hook', async () => {
const collection = db.collection({
name: 'tests',
schema: [
{ type: 'string', name: 'name' },
],
});
await db.sync();
collection.model.beforeCreate((model) => {
const changed = model.changed();
for (const name of changed || []) {
model.set(name, `${model.get(name)}111`);
}
});
collection.schema.set('name2', { type: 'string' });
await db.sync();
const model = await collection.model.create({
name: 'n1',
name2: 'n2',
});
expect(model.toJSON()).toMatchObject({
name: 'n1111',
name2: 'n2111',
});
});
});

View File

@ -0,0 +1,59 @@
import { ModelCtor, Model } from 'sequelize';
import { Database } from './database';
import { Schema } from './schema';
import { RelationField } from './schema-fields';
import _ from 'lodash';
export interface CollectionOptions {
schema?: any;
[key: string]: any;
}
export interface CollectionContext {
database: Database;
}
export class Collection {
schema: Schema;
model: ModelCtor<Model>;
options: CollectionOptions;
context: CollectionContext;
get name() {
return this.options.name;
}
constructor(options: CollectionOptions, context: CollectionContext) {
this.options = options;
this.context = context;
const { name, tableName } = options;
this.model = class extends Model<any, any> {};
const attributes = {};
this.model.init(attributes, {
..._.omit(options, ['name', 'schema']),
sequelize: context.database.sequelize,
modelName: name,
tableName: tableName || name,
});
this.schema = new Schema(options.schema, {
...context,
collection: this,
});
this.schema2model();
this.context.database.emit('collection.init', this);
}
schema2model() {
this.schema.forEach((field) => {
field.bind();
});
this.schema.on('setted', (field) => {
// console.log('setted', field);
field.bind();
});
this.schema.on('deleted', (field) => field.unbind());
this.schema.on('merged', (field) => {
//
});
}
}

View File

@ -0,0 +1,103 @@
import { Sequelize, ModelCtor, Model, Options, SyncOptions } from 'sequelize';
import { EventEmitter } from 'events';
import { Collection, CollectionOptions } from './collection';
import {
RelationField,
StringField,
HasManyField,
BelongsToField,
BelongsToManyField,
JsonField,
JsonbField,
} from './schema-fields';
export interface PendingOptions {
field: RelationField;
model: ModelCtor<Model>;
}
export type DatabaseOptions = Options | Sequelize;
export class Database extends EventEmitter {
sequelize: Sequelize;
schemaTypes = new Map();
collections: Map<string, Collection>;
pendingFields = new Map<string, RelationField[]>();
constructor(options: DatabaseOptions) {
super();
if (options instanceof Sequelize) {
this.sequelize = options;
} else {
this.sequelize = new Sequelize(options);
}
this.collections = new Map();
this.on('collection.init', (collection) => {
const items = this.pendingFields.get(collection.name);
for (const field of items || []) {
field.bind();
}
});
this.registerSchemaTypes({
string: StringField,
json: JsonField,
jsonb: JsonbField,
hasMany: HasManyField,
belongsTo: BelongsToField,
belongsToMany: BelongsToManyField,
});
}
collection(options: CollectionOptions) {
let collection = this.collections.get(options.name);
if (collection) {
collection.schema.set(options.schema);
} else {
collection = new Collection(options, { database: this });
}
this.collections.set(collection.name, collection);
return collection;
}
getCollection(name: string) {
return this.collections.get(name);
}
addPendingField(field: RelationField) {
const associating = this.pendingFields;
const items = this.pendingFields.get(field.target) || [];
items.push(field);
associating.set(field.target, items);
}
removePendingField(field: RelationField) {
const items = this.pendingFields.get(field.target) || [];
const index = items.findIndex(
(item) => item && item.name === field.name,
);
if (index !== -1) {
delete items[index];
this.pendingFields.set(field.target, items);
}
}
registerSchemaTypes(schemaTypes: any) {
for (const [type, schemaType] of Object.entries(schemaTypes)) {
this.schemaTypes.set(type, schemaType);
}
}
buildSchemaField(options, context) {
const { type } = options;
const Field = this.schemaTypes.get(type);
return new Field(options, context);
}
async sync(options?: SyncOptions) {
return this.sequelize.sync(options);
}
async close() {
return this.sequelize.close();
}
}

View File

View File

@ -0,0 +1,53 @@
import { omit } from 'lodash';
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { RelationField } from './relation-field';
export class BelongsToField extends RelationField {
get target() {
const { target, name } = this.options;
return target || Utils.pluralize(name);
}
bind() {
const { database, collection } = this.context;
const Target = this.TargetModel;
if (!Target) {
database.addPendingField(this);
return false;
}
const association = collection.model.belongsTo(Target, {
as: this.name,
...omit(this.options, ['name', 'type', 'target']),
});
// 建立关系之后从 pending 列表中删除
database.removePendingField(this);
if (!this.options.foreignKey) {
this.options.foreignKey = association.foreignKey;
}
if (!this.options.sourceKey) {
// @ts-ignore
this.options.sourceKey = association.sourceKey;
}
return true;
}
unbind() {
const { database, collection } = this.context;
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
database.removePendingField(this);
// 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉
const tcoll = database.collections.get(this.target);
const foreignKey = this.options.foreignKey;
const field1 = collection.schema.get(foreignKey);
const field2 = tcoll.schema.find((field) => {
return field.type === 'hasMany' && field.foreignKey === foreignKey;
});
if (!field1 && !field2) {
collection.model.removeAttribute(foreignKey);
}
// 删掉 model 的关联字段
delete collection.model.associations[this.name];
// @ts-ignore
collection.model.refreshAttributes();
}
}

View File

@ -0,0 +1,69 @@
import { omit } from 'lodash';
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { RelationField } from './relation-field';
export class BelongsToManyField extends RelationField {
get target() {
const { target, name } = this.options;
return target || name;
}
get through() {
return (
this.options.through ||
[this.context.collection.model.name, this.target]
.map((name) => name.toLowerCase())
.sort()
.join('_')
);
}
bind() {
const { database, collection } = this.context;
const Target = this.TargetModel;
if (!Target) {
database.addPendingField(this);
return false;
}
const through = this.through;
let Through =
database.getCollection(through) ||
database.collection({
name: through,
});
const association = collection.model.belongsToMany(Target, {
...omit(this.options, ['name', 'type', 'target']),
as: this.name,
through: Through.model,
});
// 建立关系之后从 pending 列表中删除
database.removePendingField(this);
if (!this.options.foreignKey) {
this.options.foreignKey = association.foreignKey;
}
if (!this.options.sourceKey) {
this.options.sourceKey = association.sourceKey;
}
return true;
}
unbind() {
// const { database, collection } = this.context;
// // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
// database.removePendingField(this);
// // 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉
// const tcoll = database.collections.get(this.target);
// const foreignKey = this.options.foreignKey;
// const field1 = collection.schema.get(foreignKey);
// const field2 = tcoll.schema.find((field) => {
// return field.type === 'hasMany' && field.foreignKey === foreignKey;
// });
// if (!field1 && !field2) {
// collection.model.removeAttribute(foreignKey);
// }
// // 删掉 model 的关联字段
// delete collection.model.associations[this.name];
// // @ts-ignore
// collection.model.refreshAttributes();
}
}

View File

@ -0,0 +1,119 @@
import { omit } from 'lodash';
import {
Sequelize,
ModelCtor,
Model,
DataType,
AssociationScope,
ForeignKeyOptions,
HasManyOptions,
} from 'sequelize';
import { RelationField } from './relation-field';
export interface HasManyFieldOptions extends HasManyOptions {
/**
* The name of the field to use as the key for the association in the source table. Defaults to the primary
* key of the source table
*/
sourceKey?: string;
/**
* A string or a data type to represent the identifier in the table
*/
keyType?: DataType;
scope?: AssociationScope;
/**
* The alias of this model, in singular form. See also the `name` option passed to `sequelize.define`. If
* you create multiple associations between the same tables, you should provide an alias to be able to
* distinguish between them. If you provide an alias when creating the assocition, you should provide the
* same alias when eager loading and when getting associated models. Defaults to the singularized name of
* target
*/
as?: string | { singular: string; plural: string };
/**
* The name of the foreign key in the target table or an object representing the type definition for the
* foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property
* to set the name of the column. Defaults to the name of source + primary key of source
*/
foreignKey?: string | ForeignKeyOptions;
/**
* What happens when delete occurs.
*
* Cascade if this is a n:m, and set null if it is a 1:m
*
* @default 'SET NULL' or 'CASCADE'
*/
onDelete?: string;
/**
* What happens when update occurs
*
* @default 'CASCADE'
*/
onUpdate?: string;
/**
* Should on update and on delete constraints be enabled on the foreign key.
*/
constraints?: boolean;
foreignKeyConstraint?: boolean;
// scope?: AssociationScope;
/**
* If `false` the applicable hooks will not be called.
* The default value depends on the context.
*/
hooks?: boolean;
}
export class HasManyField extends RelationField {
bind() {
const { database, collection } = this.context;
const Target = this.TargetModel;
if (!Target) {
database.addPendingField(this);
return false;
}
const association = collection.model.hasMany(Target, {
as: this.name,
...omit(this.options, ['name', 'type', 'target']),
});
// 建立关系之后从 pending 列表中删除
database.removePendingField(this);
if (!this.options.foreignKey) {
this.options.foreignKey = association.foreignKey;
}
if (!this.options.sourceKey) {
// @ts-ignore
this.options.sourceKey = association.sourceKey;
}
return true;
}
unbind() {
const { database, collection } = this.context;
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
database.removePendingField(this);
// 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉
const tcoll = database.collections.get(this.target);
const foreignKey = this.options.foreignKey;
const field = tcoll.schema.find((field) => {
if (field.name === foreignKey) {
return true;
}
return field.type === 'belongsTo' && field.foreignKey === foreignKey;
});
if (!field) {
tcoll.model.removeAttribute(foreignKey);
}
// 删掉 model 的关联字段
delete collection.model.associations[this.name];
// @ts-ignore
collection.model.refreshAttributes();
}
}

View File

@ -0,0 +1,7 @@
export * from './schema-field';
export * from './string-field';
export * from './relation-field'
export * from './belongs-to-field'
export * from './belongs-to-many-field';
export * from './has-many-field';
export * from './json-field';

View File

@ -0,0 +1,32 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class JsonField extends SchemaField {
get dataType() {
return DataTypes.JSON;
}
toSequelize() {
return {
...this.options,
type: this.dataType,
};
}
}
export class JsonbField extends SchemaField {
get dataType() {
const dialect = this.context.database.sequelize.getDialect();
if (dialect === 'postgres') {
return DataTypes.JSONB;
}
return DataTypes.JSON;
}
toSequelize() {
return {
...this.options,
type: this.dataType,
};
}
}

View File

@ -0,0 +1,20 @@
import { SchemaField } from './schema-field';
export abstract class RelationField extends SchemaField {
get target() {
const { target, name } = this.options;
return target || name;
}
get foreignKey() {
return this.options.foreignKey;
}
get sourceKey() {
return this.options.sourceKey;
}
get TargetModel() {
return this.context.database.sequelize.models[this.target];
}
}

View File

@ -0,0 +1,65 @@
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { EventEmitter } from 'events';
import { Collection } from '../collection';
import { Database } from '../database';
import { Schema } from '../schema';
import { RelationField } from './relation-field';
import _ from 'lodash';
export interface SchemaFieldContext {
database: Database;
collection: Collection;
schema: Schema;
}
export abstract class SchemaField {
options: any;
context: SchemaFieldContext;
[key: string]: any;
get name() {
return this.options.name;
}
get type() {
return this.options.type;
}
get dataType() {
return this.options.dataType;
}
constructor(options?: any, context?: SchemaFieldContext) {
this.context = context;
this.options = options || {};
this.init();
}
init() {
// code
}
get(name: string) {
return this.options[name];
}
merge(obj: any) {
Object.assign(this.options, obj);
}
bind() {
const { model } = this.context.collection;
model.rawAttributes[this.name] = this.toSequelize();
// @ts-ignore
model.refreshAttributes();
}
unbind() {
const { model } = this.context.collection;
model.removeAttribute(this.name);
}
toSequelize(): any {
return _.omit(this.options, ['name'])
}
}

View File

@ -0,0 +1,15 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class StringField extends SchemaField {
get dataType() {
return DataTypes.STRING;
}
toSequelize() {
return {
...this.options,
type: this.dataType,
};
}
}

View File

@ -0,0 +1,80 @@
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { EventEmitter } from 'events';
import { Database } from './database';
import { Collection } from './collection';
import { SchemaField } from './schema-fields';
export interface SchemaContext {
database: Database;
collection: Collection;
}
export class Schema extends EventEmitter {
fields: Map<string, any>;
context: SchemaContext;
options: any;
constructor(options?: any, context?: SchemaContext) {
super();
this.options = options;
this.context = context;
this.fields = new Map<string, any>();
this.set(options);
}
has(name: string) {
return this.fields.has(name);
}
get(name: string) {
return this.fields.get(name);
}
set(name: string | object, obj?: any) {
if (!name) {
return this;
}
if (typeof name === 'string') {
const { database } = this.context;
const field = database.buildSchemaField({ name, ...obj }, {
...this.context,
schema: this,
model: this.context.collection.model,
});
this.fields.set(name, field);
this.emit('setted', field);
} else if (Array.isArray(name)) {
for (const value of name) {
this.set(value.name, value);
}
} else if (typeof name === 'object') {
for (const [key, value] of Object.entries(name)) {
console.log({ key, value })
this.set(key, value);
}
}
return this;
}
delete(name: string) {
const field = this.fields.get(name);
const bool = this.fields.delete(name);
this.emit('deleted', field);
return bool;
}
merge(name: string, obj) {
const field = this.get(name);
field.merge(obj);
this.emit('merged', field);
return field;
}
forEach(callback: (field: SchemaField) => void) {
return [...this.fields.values()].forEach(callback);
}
find(callback: (field: SchemaField) => boolean) {
return [...this.fields.values()].find(callback);
}
}