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
|
|
|
}
|