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