tachybase_todo/packages/database-next/src/repository.ts

454 lines
12 KiB
TypeScript
Raw Normal View History

2021-09-25 23:56:26 +08:00
import {
2021-09-27 15:28:32 +08:00
Op,
2021-09-25 23:56:26 +08:00
Model,
2021-09-27 15:28:32 +08:00
ModelCtor,
Association,
2021-09-25 23:56:26 +08:00
FindOptions,
2021-09-27 15:28:32 +08:00
BulkCreateOptions,
DestroyOptions as SequelizeDestroyOptions,
CreateOptions as SequelizeCreateOptions,
UpdateOptions as SequelizeUpdateOptions,
2021-09-25 23:56:26 +08:00
} from 'sequelize';
import { flatten } from 'flat';
import { Collection } from './collection';
import _ from 'lodash';
import { Database } from './database';
import { updateAssociations } from './update-associations';
2021-09-27 15:28:32 +08:00
import { RelationField } from './fields';
2021-09-23 00:16:04 +08:00
2021-09-25 23:56:26 +08:00
export interface IRepository {}
2021-09-23 00:16:04 +08:00
2021-09-27 15:28:32 +08:00
interface CreateManyOptions extends BulkCreateOptions {}
interface FindManyOptions extends FindOptions {
2021-09-25 23:56:26 +08:00
filter?: any;
fields?: any;
2021-09-27 15:28:32 +08:00
appends?: any;
expect?: any;
2021-09-25 23:56:26 +08:00
page?: any;
pageSize?: any;
sort?: any;
}
interface FindOneOptions extends FindOptions {
filter?: any;
fields?: any;
2021-09-27 15:28:32 +08:00
appends?: any;
expect?: any;
2021-09-25 23:56:26 +08:00
sort?: any;
2021-09-23 00:16:04 +08:00
}
2021-09-27 15:28:32 +08:00
interface CreateOptions extends SequelizeCreateOptions {
values?: any;
whitelist?: any;
blacklist?: any;
}
interface UpdateOptions extends SequelizeUpdateOptions {
values?: any;
whitelist?: any;
blacklist?: any;
}
interface DestroyOptions extends SequelizeDestroyOptions {
filter?: any;
}
interface RelatedQueryOptions {
database: Database;
field: RelationField;
source: {
idOrInstance: any;
collection: Collection;
};
target: {
association: Association & {
accessors: any;
};
collection: Collection;
};
}
type Identity = string | number;
class RelatedQuery {
options: RelatedQueryOptions;
sourceInstance: Model;
constructor(options: RelatedQueryOptions) {
this.options = options;
}
async getSourceInstance() {
if (this.sourceInstance) {
return this.sourceInstance;
}
const { idOrInstance, collection } = this.options.source;
if (idOrInstance instanceof Model) {
return (this.sourceInstance = idOrInstance);
}
this.sourceInstance = await collection.model.findByPk(idOrInstance);
return this.sourceInstance;
}
async findMany(options?: any) {
const { collection } = this.options.target;
return await collection.repository.findMany(options);
}
async findOne(options?: any) {
const { collection } = this.options.target;
return await collection.repository.findOne(options);
}
async create(values?: any, options?: any) {
const { association } = this.options.target;
const createAccessor = association.accessors.create;
const source = await this.getSourceInstance();
const instance = await source[createAccessor](values, options);
if (!instance) {
return;
}
await updateAssociations(instance, values);
return instance;
}
async update(values: any, options?: Identity | Model | UpdateOptions) {
const { association, collection } = this.options.target;
if (options instanceof Model) {
return await collection.repository.update(values, options);
}
const { field } = this.options;
if (field.type === 'hasOne' || field.type === 'belongsTo') {
const getAccessor = association.accessors.get;
const source = await this.getSourceInstance();
const instance = await source[getAccessor]();
return await collection.repository.update(values, instance);
}
// TODO
return await collection.repository.update(values, options);
}
async destroy(options?: any) {
const { association, collection } = this.options.target;
const { field } = this.options;
if (field.type === 'hasOne' || field.type === 'belongsTo') {
const getAccessor = association.accessors.get;
const source = await this.getSourceInstance();
const instance = await source[getAccessor]();
if (!instance) {
return;
}
return await collection.repository.destroy(instance.id);
}
return await collection.repository.destroy(options);
}
async set(options?: any) {}
async add(options?: any) {}
async remove(options?: any) {}
async toggle(options?: any) {}
async sync(options?: any) {}
}
class HasOneQuery extends RelatedQuery {}
class HasManyQuery extends RelatedQuery {}
class BelongsToQuery extends RelatedQuery {}
class BelongsToManyQuery extends RelatedQuery {}
2021-09-23 00:16:04 +08:00
export class Repository implements IRepository {
2021-09-25 23:56:26 +08:00
database: Database;
2021-09-27 15:28:32 +08:00
collection: Collection;
model: ModelCtor<Model>;
2021-09-23 00:16:04 +08:00
2021-09-25 23:56:26 +08:00
constructor(collection: Collection) {
this.database = collection.context.database;
this.collection = collection;
2021-09-27 15:28:32 +08:00
this.model = collection.model;
2021-09-23 00:16:04 +08:00
}
2021-09-27 15:28:32 +08:00
async findMany(options?: FindManyOptions) {
2021-09-25 23:56:26 +08:00
const model = this.collection.model;
const opts = {
subQuery: false,
2021-09-27 15:28:32 +08:00
...this.buildQueryOptions(options),
2021-09-25 23:56:26 +08:00
};
let rows = [];
if (opts.include) {
const ids = (
await model.findAll({
...opts,
includeIgnoreAttributes: false,
attributes: [model.primaryKeyAttribute],
group: `${model.name}.${model.primaryKeyAttribute}`,
})
).map((item) => item[model.primaryKeyAttribute]);
2021-09-27 15:28:32 +08:00
if (ids.length > 0) {
rows = await model.findAll({
...opts,
where: {
[model.primaryKeyAttribute]: {
[Op.in]: ids,
},
2021-09-25 23:56:26 +08:00
},
2021-09-27 15:28:32 +08:00
});
}
2021-09-25 23:56:26 +08:00
} else {
rows = await model.findAll({
...opts,
});
}
const count = await model.count({
...opts,
distinct: opts.include ? true : undefined,
});
return { count, rows };
}
2021-09-23 00:16:04 +08:00
2021-09-25 23:56:26 +08:00
async findOne(options?: FindOneOptions) {
2021-09-27 15:28:32 +08:00
const model = this.collection.model;
const opts = {
subQuery: false,
...this.buildQueryOptions(options),
};
let data: Model;
if (opts.include) {
const item = await model.findOne({
...opts,
includeIgnoreAttributes: false,
attributes: [model.primaryKeyAttribute],
group: `${model.name}.${model.primaryKeyAttribute}`,
});
if (!item) {
return;
}
data = await model.findOne({
...opts,
where: item.toJSON(),
});
} else {
data = await model.findOne({
...opts,
});
}
2021-09-25 23:56:26 +08:00
return data;
}
2021-09-23 00:16:04 +08:00
2021-09-27 15:28:32 +08:00
async create(values?: any, options?: CreateOptions) {
const instance = await this.model.create<any>(values, options);
if (!instance) {
return;
}
await updateAssociations(instance, values, options);
return instance;
}
2021-09-25 23:56:26 +08:00
2021-09-27 15:28:32 +08:00
async createMany(records: any[], options?: CreateManyOptions) {
2021-09-25 23:56:26 +08:00
const instances = await this.collection.model.bulkCreate(records, options);
const promises = instances.map((instance, index) => {
return updateAssociations(instance, records[index]);
});
return Promise.all(promises);
}
2021-09-27 15:28:32 +08:00
async update(values: any, options: Identity | Model | UpdateOptions) {
if (options instanceof Model) {
await options.update(values);
await updateAssociations(options, values);
return options;
}
let instance: Model;
if (typeof options === 'string' || typeof options === 'number') {
instance = await this.model.findByPk(options);
} else {
// TODO
instance = await this.findOne(options);
}
await instance.update(values);
await updateAssociations(instance, values);
return instance;
}
async destroy(options: Identity | Identity[] | DestroyOptions) {
if (typeof options === 'number' || typeof options === 'string') {
return await this.model.destroy({
where: {
[this.model.primaryKeyAttribute]: options,
},
});
}
if (Array.isArray(options)) {
return await this.model.destroy({
where: {
[this.model.primaryKeyAttribute]: {
[Op.in]: options,
},
},
});
}
const opts = this.buildQueryOptions(options);
return await this.model.destroy(opts);
}
// TODO
async sort() {}
relatedQuery(name: string) {
return {
for: (sourceIdOrInstance: any) => {
const field = this.collection.getField(name) as RelationField;
const database = this.collection.context.database;
const collection = database.getCollection(field.target);
const options: RelatedQueryOptions = {
field,
database: database,
source: {
collection: this.collection,
idOrInstance: sourceIdOrInstance,
},
target: {
collection,
association: this.collection.model.associations[name] as any,
},
};
switch (field.type) {
case 'hasOne':
return new HasOneQuery(options);
case 'hasMany':
return new HasManyQuery(options);
case 'belongsTo':
return new BelongsToQuery(options);
case 'belongsToMany':
return new BelongsToManyQuery(options);
}
},
};
}
buildQueryOptions(options: any) {
const opts = this.parseFilter(options.filter);
return { ...options, ...opts };
}
parseFilter(filter?: any) {
if (!filter) {
return {};
}
2021-09-25 23:56:26 +08:00
const model = this.collection.model;
2021-09-27 15:28:32 +08:00
if (typeof filter === 'number' || typeof filter === 'string') {
return {
where: {
[model.primaryKeyAttribute]: filter,
},
};
}
2021-09-25 23:56:26 +08:00
const operators = this.database.operators;
const obj = flatten(filter || {});
const include = {};
const where = {};
let skipPrefix = null;
const filter2 = {};
for (const [key, value] of Object.entries(obj)) {
_.set(filter2, key, value);
}
for (let [key, value] of Object.entries(obj)) {
if (skipPrefix && key.startsWith(skipPrefix)) {
continue;
}
let keys = key.split('.');
const associations = model.associations;
const paths = [];
const origins = [];
while (keys.length) {
const k = keys.shift();
origins.push(k);
if (k.startsWith('$')) {
if (operators.has(k)) {
const opKey = operators.get(k);
if (typeof opKey === 'symbol') {
paths.push(opKey);
continue;
} else if (typeof opKey === 'function') {
skipPrefix = origins.join('.');
// console.log({ skipPrefix }, filter2, _.get(filter2, origins));
value = opKey(_.get(filter2, origins));
break;
}
} else {
paths.push(k);
continue;
}
}
if (/\d+/.test(k)) {
paths.push(k);
continue;
}
if (!associations[k]) {
paths.push(k);
continue;
}
const associationKeys = [];
associationKeys.push(k);
_.set(include, k, {
association: k,
2021-09-27 15:28:32 +08:00
attributes: [],
2021-09-25 23:56:26 +08:00
});
let target = associations[k].target;
while (target) {
const attr = keys.shift();
if (target.rawAttributes[attr]) {
associationKeys.push(attr);
target = null;
} else if (target.associations[attr]) {
associationKeys.push(attr);
const assoc = [];
associationKeys.forEach((associationKey, index) => {
if (index > 0) {
assoc.push('include');
}
assoc.push(associationKey);
});
_.set(include, assoc, {
association: attr,
2021-09-27 15:28:32 +08:00
attributes: [],
2021-09-25 23:56:26 +08:00
});
target = target.associations[attr].target;
}
}
if (associationKeys.length > 1) {
paths.push(`$${associationKeys.join('.')}$`);
} else {
paths.push(k);
}
}
console.log(paths, value);
const values = _.get(where, paths);
if (
values &&
typeof values === 'object' &&
value &&
typeof value === 'object'
) {
value = { ...value, ...values };
}
_.set(where, paths, value);
}
const toInclude = (items) => {
return Object.values(items).map((item: any) => {
if (item.include) {
item.include = toInclude(item.include);
}
return item;
});
};
2021-09-27 15:28:32 +08:00
return { where, include: toInclude(include) };
2021-09-25 23:56:26 +08:00
}
2021-09-23 00:16:04 +08:00
}