feat: add repository
This commit is contained in:
parent
9d67ecaff0
commit
4651d3dddd
@ -20,7 +20,7 @@ export function getConfig(config = {}, options?: any): DatabaseOptions {
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: process.env.DB_DIALECT,
|
||||
logging: process.env.DB_LOG_SQL === 'on',
|
||||
// logging: process.env.DB_LOG_SQL === 'on',
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
|
156
packages/collections/src/__tests__/repository.test.ts
Normal file
156
packages/collections/src/__tests__/repository.test.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { Collection } from '../collection';
|
||||
import { Database } from '../database';
|
||||
import { updateAssociation, updateAssociations } from '../update-associations';
|
||||
import { mockDatabase } from './';
|
||||
|
||||
describe('repository', () => {
|
||||
let db: Database;
|
||||
let User: Collection;
|
||||
let Post: Collection;
|
||||
let Comment: Collection;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'posts' },
|
||||
],
|
||||
});
|
||||
Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
],
|
||||
});
|
||||
Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
await db.sync();
|
||||
await User.repository.bulkCreate([
|
||||
{
|
||||
name: 'user1',
|
||||
posts: [
|
||||
{
|
||||
name: 'post11',
|
||||
comments: [
|
||||
{ name: 'comment111' },
|
||||
{ name: 'comment112' },
|
||||
{ name: 'comment113' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'post12',
|
||||
comments: [
|
||||
{ name: 'comment121' },
|
||||
{ name: 'comment122' },
|
||||
{ name: 'comment123' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'post13',
|
||||
comments: [
|
||||
{ name: 'comment131' },
|
||||
{ name: 'comment132' },
|
||||
{ name: 'comment133' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'post14',
|
||||
comments: [
|
||||
{ name: 'comment141' },
|
||||
{ name: 'comment142' },
|
||||
{ name: 'comment143' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'user2',
|
||||
posts: [
|
||||
{
|
||||
name: 'post21',
|
||||
comments: [
|
||||
{ name: 'comment211' },
|
||||
{ name: 'comment212' },
|
||||
{ name: 'comment213' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'post22',
|
||||
comments: [
|
||||
{ name: 'comment221' },
|
||||
{ name: 'comment222' },
|
||||
{ name: 'comment223' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'post23',
|
||||
comments: [
|
||||
{ name: 'comment231' },
|
||||
{ name: 'comment232' },
|
||||
{ name: 'comment233' },
|
||||
],
|
||||
},
|
||||
{ name: 'post24' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'user3',
|
||||
posts: [
|
||||
{
|
||||
name: 'post31',
|
||||
comments: [
|
||||
{ name: 'comment311' },
|
||||
{ name: 'comment312' },
|
||||
{ name: 'comment313' },
|
||||
],
|
||||
},
|
||||
{ name: 'post32' },
|
||||
{
|
||||
name: 'post33',
|
||||
comments: [
|
||||
{ name: 'comment331' },
|
||||
{ name: 'comment332' },
|
||||
{ name: 'comment333' },
|
||||
],
|
||||
},
|
||||
{ name: 'post34' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it.only('findAll', async () => {
|
||||
const data = await User.repository.findAll({
|
||||
filter: {
|
||||
'posts.comments.id': null,
|
||||
},
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
});
|
||||
console.log(data.count, JSON.stringify(data.rows.map(row => row.toJSON()), null, 2));
|
||||
// expect(data.toJSON()).toMatchObject({
|
||||
// name: 'user3',
|
||||
// });
|
||||
});
|
||||
|
||||
it('findOne', async () => {
|
||||
const data = await User.repository.findOne({
|
||||
filter: {
|
||||
'posts.comments.name': 'comment331',
|
||||
},
|
||||
});
|
||||
// expect(data.toJSON()).toMatchObject({
|
||||
// name: 'user3',
|
||||
// });
|
||||
});
|
||||
|
||||
});
|
@ -3,6 +3,7 @@ import { Database } from './database';
|
||||
import { Schema } from './schema';
|
||||
import { RelationField } from './schema-fields';
|
||||
import _ from 'lodash';
|
||||
import { Repository } from './repository';
|
||||
|
||||
export interface CollectionOptions {
|
||||
schema?: any;
|
||||
@ -16,6 +17,7 @@ export interface CollectionContext {
|
||||
export class Collection {
|
||||
schema: Schema;
|
||||
model: ModelCtor<Model>;
|
||||
repository: Repository;
|
||||
options: CollectionOptions;
|
||||
context: CollectionContext;
|
||||
|
||||
@ -44,6 +46,7 @@ export class Collection {
|
||||
});
|
||||
this.schema2model();
|
||||
this.context.database.emit('collection.init', this);
|
||||
this.repository = new Repository(this);
|
||||
}
|
||||
|
||||
schema2model() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Sequelize, ModelCtor, Model, Options, SyncOptions } from 'sequelize';
|
||||
import { Sequelize, ModelCtor, Model, Options, SyncOptions, Op, Utils } from 'sequelize';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Collection, CollectionOptions } from './collection';
|
||||
import {
|
||||
@ -24,6 +24,7 @@ export class Database extends EventEmitter {
|
||||
schemaTypes = new Map();
|
||||
models = new Map();
|
||||
repositories = new Map();
|
||||
operators = new Map();
|
||||
collections: Map<string, Collection>;
|
||||
pendingFields = new Map<string, RelationField[]>();
|
||||
|
||||
@ -50,6 +51,18 @@ export class Database extends EventEmitter {
|
||||
belongsTo: BelongsToField,
|
||||
belongsToMany: BelongsToManyField,
|
||||
});
|
||||
|
||||
const operators = new Map();
|
||||
|
||||
// Sequelize 内置
|
||||
for (const key in Op) {
|
||||
operators.set('$' + key, Op[key]);
|
||||
const val = Utils.underscoredIf(key, true);
|
||||
operators.set('$' + val, Op[key]);
|
||||
operators.set('$' + val.replace(/_/g, ''), Op[key]);
|
||||
}
|
||||
|
||||
this.operators = operators;
|
||||
}
|
||||
|
||||
collection(options: CollectionOptions) {
|
||||
@ -101,6 +114,12 @@ export class Database extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
registerOperators(operators) {
|
||||
for (const [key, operator] of Object.entries(operators)) {
|
||||
this.operators.set(key, operator);
|
||||
}
|
||||
}
|
||||
|
||||
buildSchemaField(options, context) {
|
||||
const { type } = options;
|
||||
const Field = this.schemaTypes.get(type);
|
||||
|
@ -1,23 +1,199 @@
|
||||
import { ModelCtor, Model } from 'sequelize';
|
||||
import {
|
||||
ModelCtor,
|
||||
Model,
|
||||
BulkCreateOptions,
|
||||
FindOptions,
|
||||
Op,
|
||||
} from 'sequelize';
|
||||
import { flatten } from 'flat';
|
||||
import { Collection } from './collection';
|
||||
import _ from 'lodash';
|
||||
import { Database } from './database';
|
||||
import { updateAssociations } from './update-associations';
|
||||
|
||||
export interface IRepository {
|
||||
export interface IRepository {}
|
||||
|
||||
interface FindAllOptions extends FindOptions {
|
||||
filter?: any;
|
||||
fields?: any;
|
||||
page?: any;
|
||||
pageSize?: any;
|
||||
sort?: any;
|
||||
}
|
||||
|
||||
interface FindOneOptions extends FindOptions {
|
||||
filter?: any;
|
||||
fields?: any;
|
||||
sort?: any;
|
||||
}
|
||||
|
||||
export class Repository implements IRepository {
|
||||
model: ModelCtor<Model>;
|
||||
collection: Collection;
|
||||
database: Database;
|
||||
|
||||
constructor(model: ModelCtor<Model>) {
|
||||
this.model = model;
|
||||
constructor(collection: Collection) {
|
||||
this.database = collection.context.database;
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
findAll() {}
|
||||
async findAll(options?: FindAllOptions) {
|
||||
const model = this.collection.model;
|
||||
const opts = {
|
||||
subQuery: false,
|
||||
...this.parseApiJson(options),
|
||||
};
|
||||
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]);
|
||||
rows = await model.findAll({
|
||||
...opts,
|
||||
where: {
|
||||
[model.primaryKeyAttribute]: {
|
||||
[Op.in]: ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
rows = await model.findAll({
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
const count = await model.count({
|
||||
...opts,
|
||||
distinct: opts.include ? true : undefined,
|
||||
});
|
||||
return { count, rows };
|
||||
}
|
||||
|
||||
findOne() {}
|
||||
async findOne(options?: FindOneOptions) {
|
||||
const opts = this.parseApiJson(options);
|
||||
console.log({ opts });
|
||||
const data = await this.collection.model.findOne(opts);
|
||||
return data;
|
||||
}
|
||||
|
||||
create() {}
|
||||
|
||||
update() {}
|
||||
|
||||
destroy() {}
|
||||
|
||||
async bulkCreate(records: any[], options?: BulkCreateOptions) {
|
||||
const instances = await this.collection.model.bulkCreate(records, options);
|
||||
const promises = instances.map((instance, index) => {
|
||||
return updateAssociations(instance, records[index]);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
parseApiJson(options: any) {
|
||||
const filter = options.filter || {};
|
||||
const model = this.collection.model;
|
||||
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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
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;
|
||||
});
|
||||
};
|
||||
console.log(JSON.stringify({ include: toInclude(include) }, null, 2));
|
||||
return { ...options, where, include: toInclude(include) };
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user