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,
|
host: process.env.DB_HOST,
|
||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT,
|
||||||
dialect: process.env.DB_DIALECT,
|
dialect: process.env.DB_DIALECT,
|
||||||
logging: process.env.DB_LOG_SQL === 'on',
|
// logging: process.env.DB_LOG_SQL === 'on',
|
||||||
sync: {
|
sync: {
|
||||||
force: true,
|
force: true,
|
||||||
alter: {
|
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 { Schema } from './schema';
|
||||||
import { RelationField } from './schema-fields';
|
import { RelationField } from './schema-fields';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { Repository } from './repository';
|
||||||
|
|
||||||
export interface CollectionOptions {
|
export interface CollectionOptions {
|
||||||
schema?: any;
|
schema?: any;
|
||||||
@ -16,6 +17,7 @@ export interface CollectionContext {
|
|||||||
export class Collection {
|
export class Collection {
|
||||||
schema: Schema;
|
schema: Schema;
|
||||||
model: ModelCtor<Model>;
|
model: ModelCtor<Model>;
|
||||||
|
repository: Repository;
|
||||||
options: CollectionOptions;
|
options: CollectionOptions;
|
||||||
context: CollectionContext;
|
context: CollectionContext;
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ export class Collection {
|
|||||||
});
|
});
|
||||||
this.schema2model();
|
this.schema2model();
|
||||||
this.context.database.emit('collection.init', this);
|
this.context.database.emit('collection.init', this);
|
||||||
|
this.repository = new Repository(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
schema2model() {
|
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 { EventEmitter } from 'events';
|
||||||
import { Collection, CollectionOptions } from './collection';
|
import { Collection, CollectionOptions } from './collection';
|
||||||
import {
|
import {
|
||||||
@ -24,6 +24,7 @@ export class Database extends EventEmitter {
|
|||||||
schemaTypes = new Map();
|
schemaTypes = new Map();
|
||||||
models = new Map();
|
models = new Map();
|
||||||
repositories = new Map();
|
repositories = new Map();
|
||||||
|
operators = new Map();
|
||||||
collections: Map<string, Collection>;
|
collections: Map<string, Collection>;
|
||||||
pendingFields = new Map<string, RelationField[]>();
|
pendingFields = new Map<string, RelationField[]>();
|
||||||
|
|
||||||
@ -50,6 +51,18 @@ export class Database extends EventEmitter {
|
|||||||
belongsTo: BelongsToField,
|
belongsTo: BelongsToField,
|
||||||
belongsToMany: BelongsToManyField,
|
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) {
|
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) {
|
buildSchemaField(options, context) {
|
||||||
const { type } = options;
|
const { type } = options;
|
||||||
const Field = this.schemaTypes.get(type);
|
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 {
|
export class Repository implements IRepository {
|
||||||
model: ModelCtor<Model>;
|
collection: Collection;
|
||||||
|
database: Database;
|
||||||
|
|
||||||
constructor(model: ModelCtor<Model>) {
|
constructor(collection: Collection) {
|
||||||
this.model = model;
|
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() {}
|
create() {}
|
||||||
|
|
||||||
update() {}
|
update() {}
|
||||||
|
|
||||||
destroy() {}
|
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