diff --git a/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts b/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts index 4979571c9..18f5e68da 100644 --- a/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts +++ b/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts @@ -33,6 +33,7 @@ export abstract class SequelizeSchema implements SchemaAdapter> model: ModelStatic; fieldHash: string; excludedFields: string[]; + synced: boolean; readonly idField; protected constructor( @@ -176,7 +177,7 @@ export abstract class SequelizeSchema implements SchemaAdapter> if (this.associations) { assocs = extractAssociationsFromObject(parsedQuery, this.associations); } - const relationObjects = this.extractManyRelationsModification(parsedQuery); + const relationObjectsArray = this.extractManyRelationsModification(parsedQuery); const t = await this.sequelize.transaction({ type: Transaction.TYPES.IMMEDIATE }); return this.model .bulkCreate(parsedQuery, { @@ -186,7 +187,7 @@ export abstract class SequelizeSchema implements SchemaAdapter> .then(docs => { return Promise.all( docs.map((doc, index) => - this.createWithPopulation(doc, relationObjects[index], t), + this.createWithPopulation(doc, relationObjectsArray[index], t), ), ); }) @@ -233,6 +234,18 @@ export abstract class SequelizeSchema implements SchemaAdapter> } } } + for (const relation in this.extractedRelations) { + if (!this.extractedRelations.hasOwnProperty(relation)) continue; + const value = this.extractedRelations[relation]; + // many-to-many relations cannot be null + if (!Array.isArray(value)) continue; + const item = value[0]; + const name = this.model.name + '_' + item.originalSchema.name; + promiseChain = promiseChain.then(() => + this.sequelize.models[name].sync({ alter: { drop: false } }), + ); + } + promiseChain = promiseChain.then(() => (this.synced = true)); return promiseChain; } @@ -340,9 +353,14 @@ export abstract class SequelizeSchema implements SchemaAdapter> for (const target in parsedQuery) { if (!parsedQuery.hasOwnProperty(target)) continue; if (this.extractedRelations.hasOwnProperty(target)) { - // @ts-ignore - relationObjects[target] = parsedQuery[target]; - delete parsedQuery[target]; + if (Array.isArray(parsedQuery[target])) { + // @ts-ignore + relationObjects[target] = parsedQuery[target]; + delete parsedQuery[target]; + } else { + parsedQuery[target + 'Id'] = parsedQuery[target]; + delete parsedQuery[target]; + } } } return relationObjects; diff --git a/modules/database/src/adapters/sequelize-adapter/parser/index.ts b/modules/database/src/adapters/sequelize-adapter/parser/index.ts index 537a4bae0..58c3c007d 100644 --- a/modules/database/src/adapters/sequelize-adapter/parser/index.ts +++ b/modules/database/src/adapters/sequelize-adapter/parser/index.ts @@ -199,10 +199,15 @@ function handleRelation( ) { const relationKey = key.indexOf('.') !== -1 ? key.split('.')[0] : key; if (relations && relations[relationKey]) { - if (requiredRelations.indexOf(key) === -1) { - requiredRelations.push(key); + // many-to-many relations and querying of fields other than id + if (Array.isArray(relations[key]) || key.indexOf('.') !== -1) { + if (requiredRelations.indexOf(key) === -1) { + requiredRelations.push(key); + } + return { [`$${key}${key.indexOf('.') !== -1 ? '' : '._id'}$`]: value }; + } else { + return { [`${key}Id`]: value }; } - return { [`$${key}${key.indexOf('.') !== -1 ? '' : '._id'}$`]: value }; } } @@ -286,6 +291,7 @@ function parseSelect( if (!Array.isArray(relations[tmp])) { // @ts-ignore include.push([tmp + 'Id', tmp]); + exclude.push(tmp + 'Id'); } else { include.push(tmp); } @@ -330,19 +336,22 @@ function parseSelect( export function renameRelations( population: string[], relations: { [key: string]: SequelizeSchema | SequelizeSchema[] }, -): { include: string[] } { +): { include: string[]; exclude: string[] } { const include: string[] = []; + const exclude: string[] = []; for (const relation in relations) { if (population.indexOf(relation) !== -1) continue; if (!Array.isArray(relations[relation])) { // @ts-ignore include.push([relation + 'Id', relation]); + exclude.push(relation + 'Id'); } } return { include, + exclude, }; } diff --git a/modules/database/src/adapters/sequelize-adapter/postgres-adapter/index.ts b/modules/database/src/adapters/sequelize-adapter/postgres-adapter/index.ts index df3aa17e5..505edba6d 100644 --- a/modules/database/src/adapters/sequelize-adapter/postgres-adapter/index.ts +++ b/modules/database/src/adapters/sequelize-adapter/postgres-adapter/index.ts @@ -67,7 +67,7 @@ export class PostgresAdapter extends SequelizeAdapter { const rel = Array.isArray(extractedRelations[relation]) ? (extractedRelations[relation] as any[])[0] : extractedRelations[relation]; - if (!this.models[rel.model]) { + if (!this.models[rel.model] || !this.models[rel.model].synced) { if (!pendingModels.includes(rel.model)) { pendingModels.push(rel.model); } @@ -87,7 +87,7 @@ export class PostgresAdapter extends SequelizeAdapter { while (pendingModels.length > 0) { await sleep(500); pendingModels = pendingModels.filter(model => { - if (!this.models[model]) { + if (!this.models[model] || !this.models[model].synced) { return true; } else { for (const schema in relatedSchemas) { diff --git a/modules/database/src/adapters/sequelize-adapter/sql-adapter/index.ts b/modules/database/src/adapters/sequelize-adapter/sql-adapter/index.ts index 81790d6f3..1cff6d6d6 100644 --- a/modules/database/src/adapters/sequelize-adapter/sql-adapter/index.ts +++ b/modules/database/src/adapters/sequelize-adapter/sql-adapter/index.ts @@ -93,7 +93,7 @@ export class SQLAdapter extends SequelizeAdapter { const rel = Array.isArray(extractedRelations[relation]) ? (extractedRelations[relation] as any[])[0] : extractedRelations[relation]; - if (!this.models[rel.model]) { + if (!this.models[rel.model] || !this.models[rel.model].synced) { if (!pendingModels.includes(rel.model)) { pendingModels.push(rel.model); } @@ -113,7 +113,7 @@ export class SQLAdapter extends SequelizeAdapter { while (pendingModels.length > 0) { await sleep(500); pendingModels = pendingModels.filter(model => { - if (!this.models[model]) { + if (!this.models[model] || !this.models[model].synced) { return true; } else { for (const schema in relatedSchemas) { diff --git a/modules/database/src/adapters/sequelize-adapter/utils/schema.ts b/modules/database/src/adapters/sequelize-adapter/utils/schema.ts index 6980d7b73..190745885 100644 --- a/modules/database/src/adapters/sequelize-adapter/utils/schema.ts +++ b/modules/database/src/adapters/sequelize-adapter/utils/schema.ts @@ -1,7 +1,6 @@ import { DataTypes, ModelStatic, Sequelize } from 'sequelize'; import { ConduitSchema, Indexable } from '@conduitplatform/grpc-sdk'; import { SequelizeSchema } from '../SequelizeSchema'; -import assert from 'assert'; const deepdash = require('deepdash/standalone'); @@ -14,42 +13,57 @@ export const extractRelations = ( for (const relation in relations) { if (relations.hasOwnProperty(relation)) { const value = relations[relation]; + // many-to-many relations cannot be null if (Array.isArray(value)) { const item = value[0]; - model.belongsToMany(item.model, { - foreignKey: item.originalSchema.name, - // foreignKey: { - // name: item.originalSchema.name, - // allowNull: !((originalSchema.fields[relation] as any[])[0] as any).required, - // defaultValue: ((originalSchema.fields[relation] as any[])[0] as any).default, - // }, - as: relation, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - through: model.name + '_' + item.originalSchema.name, - }); - item.model.belongsToMany(model, { - foreignKey: name, - // foreignKey: { - // name, - // allowNull: !((originalSchema.fields[relation] as any[])[0] as any).required, - // defaultValue: ((originalSchema.fields[relation] as any[])[0] as any).default, - // }, - as: relation, - through: model.name + '_' + item.originalSchema.name, - }); - item.sync(); + if ( + item.model.associations[relation] && + item.model.associations[relation].foreignKey === name + ) { + model.belongsToMany(item.model, { + foreignKey: item.originalSchema.name, + as: relation, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + through: model.name + '_' + item.originalSchema.name, + }); + continue; + } else if ( + item.model.associations[relation] && + item.model.associations[relation].foreignKey !== name + ) { + throw new Error( + `Relation ${relation} already exists on ${item.model.name} with a different foreign key`, + ); + } else { + model.belongsToMany(item.model, { + foreignKey: item.originalSchema.name, + as: relation, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + through: model.name + '_' + item.originalSchema.name, + }); + item.model.belongsToMany(model, { + foreignKey: name, + as: relation, + through: model.name + '_' + item.originalSchema.name, + }); + item.sync(); + } } else { model.belongsTo(value.model, { - foreignKey: relation + 'Id', - // foreignKey: { - // name: relation + 'Id', - // allowNull: !(originalSchema.fields[relation] as any).required, - // defaultValue: (originalSchema.fields[relation] as any).default, - // }, + foreignKey: { + name: relation + 'Id', + allowNull: !(originalSchema.fields[relation] as any).required, + defaultValue: (originalSchema.fields[relation] as any).default, + }, as: relation, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', + onUpdate: (originalSchema.fields[relation] as any).required + ? 'CASCADE' + : 'NO ACTION', + onDelete: (originalSchema.fields[relation] as any).required + ? 'CASCADE' + : 'SET NULL', }); } }