feat: limit database identifier (#908)
This commit is contained in:
parent
76f5754e20
commit
eaea8a7100
@ -1,6 +1,7 @@
|
||||
import { Collection } from '../collection';
|
||||
import { Database } from '../database';
|
||||
import { mockDatabase } from './index';
|
||||
import { IdentifierError } from '../errors/identifier-error';
|
||||
|
||||
describe('collection', () => {
|
||||
let db: Database;
|
||||
@ -258,4 +259,50 @@ describe('collection sync', () => {
|
||||
expect(tableFields['postId']).toBeDefined();
|
||||
expect(tableFields['tagId']).toBeDefined();
|
||||
});
|
||||
|
||||
test('limit table name length', async () => {
|
||||
const longName =
|
||||
'this_is_a_very_long_table_name_that_should_be_truncated_this_is_a_very_long_table_name_that_should_be_truncated';
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
const collection = new Collection(
|
||||
{
|
||||
name: longName,
|
||||
fields: [{ type: 'string', name: 'test' }],
|
||||
},
|
||||
{
|
||||
database: db,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(IdentifierError);
|
||||
});
|
||||
|
||||
test('limit field name length', async () => {
|
||||
const longFieldName =
|
||||
'this_is_a_very_long_field_name_that_should_be_truncated_this_is_a_very_long_field_name_that_should_be_truncated';
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
const collection = new Collection(
|
||||
{
|
||||
name: 'test',
|
||||
fields: [{ type: 'string', name: longFieldName }],
|
||||
},
|
||||
{
|
||||
database: db,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(IdentifierError);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
import { IdentifierError } from '../../errors/identifier-error';
|
||||
|
||||
describe('belongs to field', () => {
|
||||
let db: Database;
|
||||
@ -28,12 +29,16 @@ describe('belongs to field', () => {
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(Comment.model.associations.post).toBeUndefined();
|
||||
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [{ type: 'string', name: 'title' }],
|
||||
});
|
||||
|
||||
const association = Comment.model.associations.post;
|
||||
|
||||
expect(Comment.model.associations.post).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postId');
|
||||
// @ts-ignore
|
||||
@ -77,11 +82,41 @@ describe('belongs to field', () => {
|
||||
const association = Comment.model.associations.post;
|
||||
expect(association).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postKey');
|
||||
|
||||
// @ts-ignore
|
||||
expect(association.targetKey).toBe('key');
|
||||
expect(Comment.model.rawAttributes['postKey']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw error when foreignKey is too long', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [{ type: 'string', name: 'key', unique: true }],
|
||||
});
|
||||
|
||||
const longForeignKey = 'a'.repeat(128);
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
const Comment = db.collection({
|
||||
name: 'comments1',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'post',
|
||||
targetKey: 'key',
|
||||
foreignKey: longForeignKey,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(IdentifierError);
|
||||
});
|
||||
|
||||
it('custom name and target', async () => {
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { mockDatabase } from '../';
|
||||
import { Database } from '../../database';
|
||||
import { IdentifierError } from '../../errors/identifier-error';
|
||||
|
||||
describe('belongs to many field', () => {
|
||||
let db: Database;
|
||||
@ -58,4 +59,48 @@ describe('belongs to many field', () => {
|
||||
expect(PostTag.model.rawAttributes['postId']).toBeDefined();
|
||||
expect(PostTag.model.rawAttributes['tagId']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw error when foreignKey is too long', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'tags', foreignKey: 'a'.repeat(128) },
|
||||
],
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const Tag = db.collection({
|
||||
name: 'tags',
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(IdentifierError);
|
||||
});
|
||||
|
||||
it('should throw error when through is too long', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'a'.repeat(128) },
|
||||
],
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const Tag = db.collection({
|
||||
name: 'tags',
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(IdentifierError);
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
import { makeWatchHost } from 'ts-loader/dist/servicesHost';
|
||||
import { IdentifierError } from '../../errors/identifier-error';
|
||||
|
||||
describe('has many field', () => {
|
||||
let db: Database;
|
||||
@ -149,4 +150,25 @@ describe('has many field', () => {
|
||||
Comment.removeField('post');
|
||||
expect(Comment.model.rawAttributes.postId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw error when foreignKey is too long', async () => {
|
||||
const longForeignKey = 'a'.repeat(64);
|
||||
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [{ type: 'hasMany', name: 'comments', foreignKey: longForeignKey }],
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
fields: [{ type: 'belongsTo', name: 'post' }],
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(IdentifierError);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
import { IdentifierError } from '../../errors/identifier-error';
|
||||
|
||||
describe('has many field', () => {
|
||||
let db: Database;
|
||||
@ -64,4 +65,24 @@ describe('has many field', () => {
|
||||
Profile.removeField('user');
|
||||
expect(Profile.model.rawAttributes.userId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw error when foreignKey is too long', async () => {
|
||||
const longForeignKey = 'a'.repeat(128);
|
||||
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ type: 'hasOne', name: 'profile', foreignKey: longForeignKey }],
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const Profile = db.collection({
|
||||
name: 'profiles',
|
||||
fields: [{ type: 'belongsTo', name: 'user' }],
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeInstanceOf(IdentifierError);
|
||||
});
|
||||
});
|
||||
|
@ -7,13 +7,13 @@ import {
|
||||
QueryInterfaceDropTableOptions,
|
||||
SyncOptions,
|
||||
Transactionable,
|
||||
Utils
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { Database } from './database';
|
||||
import { Field, FieldOptions } from './fields';
|
||||
import { Model } from './model';
|
||||
import { Repository } from './repository';
|
||||
import { md5 } from './utils';
|
||||
import { checkIdentifier, md5 } from './utils';
|
||||
|
||||
export type RepositoryType = typeof Repository;
|
||||
|
||||
@ -67,8 +67,11 @@ export class Collection<
|
||||
|
||||
constructor(options: CollectionOptions, context?: CollectionContext) {
|
||||
super();
|
||||
this.checkOptions(options);
|
||||
|
||||
this.context = context;
|
||||
this.options = options;
|
||||
|
||||
this.bindFieldEventListener();
|
||||
this.modelInit();
|
||||
this.setFields(options.fields);
|
||||
@ -76,6 +79,10 @@ export class Collection<
|
||||
this.setSortable(options.sortable);
|
||||
}
|
||||
|
||||
private checkOptions(options: CollectionOptions) {
|
||||
checkIdentifier(options.name);
|
||||
}
|
||||
|
||||
private sequelizeModelOptions() {
|
||||
const { name, tableName } = this.options;
|
||||
return {
|
||||
@ -160,6 +167,8 @@ export class Collection<
|
||||
}
|
||||
|
||||
setField(name: string, options: FieldOptions): Field {
|
||||
checkIdentifier(name);
|
||||
|
||||
const { database } = this.context;
|
||||
|
||||
const field = database.buildField(
|
||||
@ -169,6 +178,7 @@ export class Collection<
|
||||
collection: this,
|
||||
},
|
||||
);
|
||||
|
||||
this.removeField(name);
|
||||
this.fields.set(name, field);
|
||||
this.emit('field.afterAdd', field);
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
Sequelize,
|
||||
SyncOptions,
|
||||
Transactionable,
|
||||
Utils
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { SequelizeStorage, Umzug } from 'umzug';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
@ -361,9 +361,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
buildField(options, context: FieldContext) {
|
||||
const { type } = options;
|
||||
const Field = this.fieldTypes.get(type);
|
||||
|
||||
if (!Field) {
|
||||
throw Error(`unsupported field type ${type}`);
|
||||
}
|
||||
|
||||
return new Field(options, context);
|
||||
}
|
||||
|
||||
|
6
packages/core/database/src/errors/identifier-error.ts
Normal file
6
packages/core/database/src/errors/identifier-error.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class IdentifierError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'IdentifierError';
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { omit } from 'lodash';
|
||||
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
import { checkIdentifier } from '../utils';
|
||||
|
||||
export class BelongsToField extends RelationField {
|
||||
static type = 'belongsTo';
|
||||
@ -42,10 +43,13 @@ export class BelongsToField extends RelationField {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
|
||||
checkIdentifier(this.options.foreignKey);
|
||||
|
||||
if (!this.options.sourceKey) {
|
||||
// @ts-ignore
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
|
||||
this.collection.addIndex([this.options.foreignKey]);
|
||||
return true;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { omit } from 'lodash';
|
||||
import { BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
import { checkIdentifier } from '../utils';
|
||||
|
||||
export class BelongsToManyField extends RelationField {
|
||||
get through() {
|
||||
@ -23,6 +24,7 @@ export class BelongsToManyField extends RelationField {
|
||||
database.addPendingField(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
const through = this.through;
|
||||
|
||||
let Through: Collection;
|
||||
@ -33,6 +35,7 @@ export class BelongsToManyField extends RelationField {
|
||||
Through = database.collection({
|
||||
name: through,
|
||||
});
|
||||
|
||||
Object.defineProperty(Through.model, 'isThrough', { value: true });
|
||||
}
|
||||
|
||||
@ -49,15 +52,23 @@ export class BelongsToManyField extends RelationField {
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
|
||||
checkIdentifier(this.options.foreignKey);
|
||||
|
||||
if (!this.options.sourceKey) {
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
|
||||
if (!this.options.otherKey) {
|
||||
this.options.otherKey = association.otherKey;
|
||||
}
|
||||
|
||||
checkIdentifier(this.options.otherKey);
|
||||
|
||||
if (!this.options.through) {
|
||||
this.options.through = this.through;
|
||||
}
|
||||
|
||||
Through.addIndex([this.options.foreignKey]);
|
||||
Through.addIndex([this.options.otherKey]);
|
||||
return true;
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Database } from '../database';
|
||||
import { checkIdentifier } from '../utils';
|
||||
|
||||
export interface FieldContext {
|
||||
database: Database;
|
||||
|
@ -5,10 +5,11 @@ import {
|
||||
ForeignKeyOptions,
|
||||
HasManyOptions,
|
||||
HasManyOptions as SequelizeHasManyOptions,
|
||||
Utils
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
import { checkIdentifier } from '../utils';
|
||||
|
||||
export interface HasManyFieldOptions extends HasManyOptions {
|
||||
/**
|
||||
@ -98,6 +99,7 @@ export class HasManyField extends RelationField {
|
||||
as: this.name,
|
||||
foreignKey: this.foreignKey,
|
||||
});
|
||||
|
||||
// inverse relation
|
||||
// this.TargetModel.belongsTo(collection.model);
|
||||
|
||||
@ -107,6 +109,9 @@ export class HasManyField extends RelationField {
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
|
||||
checkIdentifier(this.options.foreignKey);
|
||||
|
||||
if (!this.options.sourceKey) {
|
||||
// @ts-ignore
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
|
@ -5,10 +5,11 @@ import {
|
||||
ForeignKeyOptions,
|
||||
HasOneOptions,
|
||||
HasOneOptions as SequelizeHasOneOptions,
|
||||
Utils
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
import { checkIdentifier } from '../utils';
|
||||
|
||||
export interface HasOneFieldOptions extends HasOneOptions {
|
||||
/**
|
||||
@ -92,21 +93,28 @@ export class HasOneField extends RelationField {
|
||||
database.addPendingField(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
const association = collection.model.hasOne(Target, {
|
||||
constraints: false,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
as: this.name,
|
||||
foreignKey: this.foreignKey,
|
||||
});
|
||||
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
|
||||
checkIdentifier(this.options.foreignKey);
|
||||
|
||||
if (!this.options.sourceKey) {
|
||||
// @ts-ignore
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
|
||||
let tcoll: Collection;
|
||||
if (this.target === collection.name) {
|
||||
tcoll = collection;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
import { Model } from './model';
|
||||
import { IdentifierError } from './errors/identifier-error';
|
||||
|
||||
type HandleAppendsQueryOptions = {
|
||||
templateModel: any;
|
||||
@ -49,3 +50,11 @@ export async function handleAppendsQuery(options: HandleAppendsQueryOptions) {
|
||||
export function md5(value: string) {
|
||||
return crypto.createHash('md5').update(value).digest('hex');
|
||||
}
|
||||
|
||||
const MAX_IDENTIFIER_LENGTH = 63;
|
||||
|
||||
export function checkIdentifier(value: string) {
|
||||
if (value.length > MAX_IDENTIFIER_LENGTH) {
|
||||
throw new IdentifierError(`Identifier ${value} is too long`);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user