From e6f3ba8ec8f6315213121ac0039ca06382d75359 Mon Sep 17 00:00:00 2001 From: Ben Werner <42802102+benwerner01@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:49:07 +0100 Subject: [PATCH] Add `aggregationId` for aggregations (#465) * ft: add aggregation id migration script * refactor: update aggregation pg adapter methods * ft: get aggregation using id pg adapter method * refactor: aggregation model class to use updated pg adapter methods/params * chore: aggregation db adapter method documentation * chore: todo comments * refactor: simply step 2 of add aggregation id migration script * ft: add aggregationId to `LinkedAggregation` in GQL API * fix: remove redundant part of `getAggregation` sql qeury * fix: add `source_account_id` to primary key of aggregations table --- packages/hash/api/src/db/adapter.ts | 58 +++++++++++++++--- packages/hash/api/src/db/errors.ts | 20 ++----- .../postgres/aggregation/createAggregation.ts | 26 ++++---- .../postgres/aggregation/deleteAggregation.ts | 22 ++++--- .../db/postgres/aggregation/getAggregation.ts | 22 +++++++ ...ation.ts => getEntityAggregationByPath.ts} | 13 ++-- .../aggregation/updateAggregationOperation.ts | 59 ++++++++----------- .../api/src/db/postgres/aggregation/util.ts | 27 ++++----- packages/hash/api/src/db/postgres/client.ts | 17 ++++-- packages/hash/api/src/db/postgres/entity.ts | 26 +++----- packages/hash/api/src/db/postgres/index.ts | 14 +++-- .../deleteLinkedAggregation.ts | 2 +- .../linkedAggregationResults.ts | 2 +- .../updateLinkedAggregationOperation.ts | 2 +- .../graphql/typeDefs/aggregation.typedef.ts | 1 + .../hash/api/src/model/aggregation.model.ts | 45 +++++++++----- packages/hash/api/src/model/entity.model.ts | 4 +- .../1649061269485_add-aggregation-id.ts | 59 +++++++++++++++++++ .../postgres/scripts/generate-ids.ts | 5 +- packages/hash/datastore/postgres/util.ts | 29 +++++++++ .../integration/src/tests/integration.test.ts | 6 +- 21 files changed, 300 insertions(+), 159 deletions(-) create mode 100644 packages/hash/api/src/db/postgres/aggregation/getAggregation.ts rename packages/hash/api/src/db/postgres/aggregation/{getEntityAggregation.ts => getEntityAggregationByPath.ts} (76%) create mode 100644 packages/hash/datastore/postgres/migration/1649061269485_add-aggregation-id.ts diff --git a/packages/hash/api/src/db/adapter.ts b/packages/hash/api/src/db/adapter.ts index ad943ec9f54..ce945bbbb17 100644 --- a/packages/hash/api/src/db/adapter.ts +++ b/packages/hash/api/src/db/adapter.ts @@ -114,6 +114,7 @@ export type DbLinkVersion = { }; export type DbAggregation = { + aggregationId: string; sourceAccountId: string; sourceEntityId: string; sourceEntityVersionIds: Set; @@ -541,39 +542,80 @@ export interface DbClient { emailAddress: string; }): Promise; + /** + * Create an aggregation for an entity. + * + * @param params.sourceAccountId the account id of the source entity + * @param params.sourceEntityId the entity id of the source entity + * @param params.path the aggregation path + * @param params.operation the aggregation operation + * @param params.createdByAccountId the account id of the user that created the aggregation + */ createAggregation(params: { - createdByAccountId: string; sourceAccountId: string; sourceEntityId: string; path: string; operation: object; + createdByAccountId: string; }): Promise; + /** + * Update the operation of an existing aggregation. + * + * @param params.aggregationId the id of the aggregation + * @param params.operation the updated aggregation operation + */ updateAggregationOperation(params: { - sourceAccountId: string; - sourceEntityId: string; - path: string; + aggregationId: string; operation: object; }): Promise; - getEntityAggregation(params: { + /** + * Get an aggregation by its id. + * + * @param params.aggregationId the id of the aggregation + */ + getAggregation(params: { + aggregationId: string; + }): Promise; + + /** + * Get an aggregation by its source entity and path. + * + * @param params.sourceAccountId the account id of the source entity + * @param params.sourceEntityId the entity id of the source entity + * @param params.sourceEntityVersionId the entityVersionId of the source entity (optional) + * @param params.path the aggregation path + */ + getEntityAggregationByPath(params: { sourceAccountId: string; sourceEntityId: string; sourceEntityVersionId?: string; path: string; }): Promise; + /** + * Get all aggregations for an entity. + * + * @param params.sourceAccountId the account id of the source entity + * @param params.sourceEntityId the entity id of the source entity + * @param params.sourceEntityVersionId the entityVersionId of the source entity (optional) + */ getEntityAggregations(params: { sourceAccountId: string; sourceEntityId: string; sourceEntityVersionId?: string; }): Promise; + /** + * Delete an existing aggregation. + * + * @param params.aggregationId the id of the aggregation + * @param params.deletedByAccountId the account id of the user that deleted the aggregation + */ deleteAggregation(params: { + aggregationId: string; deletedByAccountId: string; - sourceAccountId: string; - sourceEntityId: string; - path: string; }): Promise; /** Get a verification code (it may be invalid!) */ diff --git a/packages/hash/api/src/db/errors.ts b/packages/hash/api/src/db/errors.ts index 17461d03848..14958103a8f 100644 --- a/packages/hash/api/src/db/errors.ts +++ b/packages/hash/api/src/db/errors.ts @@ -66,22 +66,12 @@ export class DbLinkNotFoundError extends Error { } export class DbAggregationNotFoundError extends Error { - sourceAccountId?: string; - sourceEntityId?: string; - path: string; + aggregationId: string; - constructor(params: { - sourceAccountId?: string; - sourceEntityId?: string; - path: string; - }) { - const { sourceAccountId, sourceEntityId, path } = params; - super( - `Aggregation with path ${path} for source entity with id ${sourceEntityId} in account ${sourceAccountId} not found`, - ); - this.sourceAccountId = sourceAccountId; - this.sourceEntityId = sourceEntityId; - this.path = path; + constructor(params: { aggregationId: string }) { + const { aggregationId } = params; + super(`Aggregation with aggregationId ${aggregationId} not found`); + this.aggregationId = aggregationId; } } diff --git a/packages/hash/api/src/db/postgres/aggregation/createAggregation.ts b/packages/hash/api/src/db/postgres/aggregation/createAggregation.ts index 1d633dafd28..dc6e4dfa130 100644 --- a/packages/hash/api/src/db/postgres/aggregation/createAggregation.ts +++ b/packages/hash/api/src/db/postgres/aggregation/createAggregation.ts @@ -1,5 +1,5 @@ import { Connection } from "../types"; -import { DbAggregation } from "../../adapter"; +import { DbAggregation, DbClient } from "../../adapter"; import { acquireEntityLock, getEntityLatestVersion, @@ -8,16 +8,11 @@ import { import { DbEntityNotFoundError } from "../.."; import { insertAggregation } from "./util"; import { requireTransaction } from "../util"; +import { genId } from "../../../util"; export const createAggregation = async ( existingConnection: Connection, - params: { - createdByAccountId: string; - sourceAccountId: string; - sourceEntityId: string; - path: string; - operation: object; - }, + params: Parameters[0], ): Promise => requireTransaction(existingConnection)(async (conn) => { const { sourceAccountId, sourceEntityId, createdByAccountId } = params; @@ -52,15 +47,16 @@ export const createAggregation = async ( const now = new Date(); - const createdAt = now; + const aggregation: DbAggregation = { + ...params, + aggregationId: genId(), + sourceEntityVersionIds, + createdAt: now, + }; await insertAggregation(conn, { - aggregation: { - ...params, - sourceEntityVersionIds, - createdAt, - }, + aggregation, }); - return { ...params, sourceEntityVersionIds, createdAt }; + return aggregation; }); diff --git a/packages/hash/api/src/db/postgres/aggregation/deleteAggregation.ts b/packages/hash/api/src/db/postgres/aggregation/deleteAggregation.ts index 0d244516d8d..9018fc2af18 100644 --- a/packages/hash/api/src/db/postgres/aggregation/deleteAggregation.ts +++ b/packages/hash/api/src/db/postgres/aggregation/deleteAggregation.ts @@ -7,18 +7,24 @@ import { import { DbEntityNotFoundError } from "../.."; import { deleteAggregationRow } from "./util"; import { requireTransaction } from "../util"; +import { getAggregation } from "./getAggregation"; +import { DbClient } from "../../adapter"; +import { DbAggregationNotFoundError } from "../../errors"; export const deleteAggregation = async ( existingConnection: Connection, - params: { - sourceAccountId: string; - sourceEntityId: string; - path: string; - deletedByAccountId: string; - }, + params: Parameters[0], ): Promise => requireTransaction(existingConnection)(async (conn) => { - const { sourceAccountId, sourceEntityId, deletedByAccountId } = params; + const { aggregationId, deletedByAccountId } = params; + + const dbAggregation = await getAggregation(conn, { aggregationId }); + + if (!dbAggregation) { + throw new DbAggregationNotFoundError(params); + } + + const { sourceAccountId, sourceEntityId } = dbAggregation; await acquireEntityLock(conn, { entityId: sourceEntityId }); @@ -47,7 +53,7 @@ export const deleteAggregation = async ( entity: dbSourceEntity, /** @todo: re-implement method to not require updated `properties` */ properties: dbSourceEntity.properties, - omittedAggregations: [params], + omittedAggregations: [{ aggregationId }], }); } else { await deleteAggregationRow(conn, params); diff --git a/packages/hash/api/src/db/postgres/aggregation/getAggregation.ts b/packages/hash/api/src/db/postgres/aggregation/getAggregation.ts new file mode 100644 index 00000000000..67b257f087b --- /dev/null +++ b/packages/hash/api/src/db/postgres/aggregation/getAggregation.ts @@ -0,0 +1,22 @@ +import { sql } from "slonik"; +import { DbAggregation, DbClient } from "../../adapter"; + +import { Connection } from "../types"; +import { + DbAggregationRow, + aggregationsColumnNamesSQL, + mapRowToDbAggregation, +} from "./util"; + +export const getAggregation = async ( + conn: Connection, + params: Parameters[0], +): Promise => { + const row = await conn.maybeOne(sql` + select ${aggregationsColumnNamesSQL} + from aggregations + where aggregation_id = ${params.aggregationId} + `); + + return row ? mapRowToDbAggregation(row) : null; +}; diff --git a/packages/hash/api/src/db/postgres/aggregation/getEntityAggregation.ts b/packages/hash/api/src/db/postgres/aggregation/getEntityAggregationByPath.ts similarity index 76% rename from packages/hash/api/src/db/postgres/aggregation/getEntityAggregation.ts rename to packages/hash/api/src/db/postgres/aggregation/getEntityAggregationByPath.ts index 7813a904014..7b31dfa00e8 100644 --- a/packages/hash/api/src/db/postgres/aggregation/getEntityAggregation.ts +++ b/packages/hash/api/src/db/postgres/aggregation/getEntityAggregationByPath.ts @@ -1,5 +1,5 @@ import { sql } from "slonik"; -import { DbAggregation } from "../../adapter"; +import { DbAggregation, DbClient } from "../../adapter"; import { Connection } from "../types"; import { @@ -8,14 +8,9 @@ import { mapRowToDbAggregation, } from "./util"; -export const getEntityAggregation = async ( +export const getEntityAggregationByPath = async ( conn: Connection, - params: { - sourceAccountId: string; - sourceEntityId: string; - sourceEntityVersionId?: string; - path: string; - }, + params: Parameters[0], ): Promise => { const row = await conn.maybeOne(sql` select ${aggregationsColumnNamesSQL} @@ -33,7 +28,7 @@ export const getEntityAggregation = async ( sql` and `, )} order by created_at desc - limit 1 + limit 1 -- @todo: remove when aggregation versions are stored in separate table `); return row ? mapRowToDbAggregation(row) : null; diff --git a/packages/hash/api/src/db/postgres/aggregation/updateAggregationOperation.ts b/packages/hash/api/src/db/postgres/aggregation/updateAggregationOperation.ts index 5213bf1bb3c..6a400907573 100644 --- a/packages/hash/api/src/db/postgres/aggregation/updateAggregationOperation.ts +++ b/packages/hash/api/src/db/postgres/aggregation/updateAggregationOperation.ts @@ -6,38 +6,27 @@ import { } from "../entity"; import { DbEntityNotFoundError } from "../.."; import { insertAggregation, updateAggregationRowOperation } from "./util"; -import { getEntityAggregation } from "./getEntityAggregation"; -import { DbAggregation } from "../../adapter"; +import { getAggregation } from "./getAggregation"; +import { DbAggregation, DbClient } from "../../adapter"; import { requireTransaction } from "../util"; import { DbAggregationNotFoundError } from "../../errors"; +import { genId } from "../../../util"; export const updateAggregationOperation = ( existingConnection: Connection, - params: { - sourceAccountId: string; - sourceEntityId: string; - path: string; - operation: object; - }, + params: Parameters[0], ): Promise => requireTransaction(existingConnection)(async (conn) => { - const { sourceAccountId, sourceEntityId, operation } = params; - const now = new Date(); - const dbAggregation = await getEntityAggregation(conn, params); + const previousDbAggregation = await getAggregation(conn, params); - if (!dbAggregation) { + if (!previousDbAggregation) { throw new DbAggregationNotFoundError(params); } - const { - createdByAccountId, - createdAt, - sourceEntityVersionIds: prevSourceEntityVersionIds, - } = dbAggregation; - - let sourceEntityVersionIds: Set = prevSourceEntityVersionIds; + const { sourceAccountId, sourceEntityId, createdByAccountId, createdAt } = + previousDbAggregation; await acquireEntityLock(conn, { entityId: sourceEntityId }); @@ -54,6 +43,15 @@ export const updateAggregationOperation = ( return dbEntity; }); + const { operation } = params; + + const updatedDbAggregation = { + ...previousDbAggregation, + operation, + createdByAccountId, + createdAt, + }; + if (dbSourceEntity.metadata.versioned) { dbSourceEntity = await updateVersionedEntity(conn, { updatedByAccountId: createdByAccountId, @@ -63,25 +61,18 @@ export const updateAggregationOperation = ( omittedAggregations: [params], }); - sourceEntityVersionIds = new Set([dbSourceEntity.entityVersionId]); + updatedDbAggregation.aggregationId = genId(); + updatedDbAggregation.sourceEntityVersionIds = new Set([ + dbSourceEntity.entityVersionId, + ]); + updatedDbAggregation.createdAt = now; await insertAggregation(conn, { - aggregation: { - ...params, - sourceEntityVersionIds, - operation, - createdAt: now, - createdByAccountId, - }, + aggregation: updatedDbAggregation, }); } else { - await updateAggregationRowOperation(conn, params); + await updateAggregationRowOperation(conn, updatedDbAggregation); } - return { - ...params, - sourceEntityVersionIds, - createdByAccountId, - createdAt, - }; + return updatedDbAggregation; }); diff --git a/packages/hash/api/src/db/postgres/aggregation/util.ts b/packages/hash/api/src/db/postgres/aggregation/util.ts index 0539f178334..0e75c686f8e 100644 --- a/packages/hash/api/src/db/postgres/aggregation/util.ts +++ b/packages/hash/api/src/db/postgres/aggregation/util.ts @@ -5,6 +5,7 @@ import { DbAggregation } from "../../adapter"; import { mapColumnNamesToSQL } from "../util"; export type DbAggregationRow = { + aggregation_id: string; source_account_id: string; source_entity_id: string; path: string; @@ -17,6 +18,7 @@ export type DbAggregationRow = { export const mapRowToDbAggregation = ( row: DbAggregationRow, ): DbAggregation => ({ + aggregationId: row.aggregation_id, sourceAccountId: row.source_account_id, sourceEntityId: row.source_entity_id, path: row.path, @@ -27,6 +29,7 @@ export const mapRowToDbAggregation = ( }); export const aggregationsColumnNames = [ + "aggregation_id", "source_account_id", "source_entity_id", "path", @@ -54,6 +57,7 @@ export const insertAggregation = async ( insert into aggregations (${aggregationsColumnNamesSQL}) values (${sql.join( [ + aggregation.aggregationId, aggregation.sourceAccountId, aggregation.sourceEntityId, aggregation.path, @@ -76,9 +80,7 @@ export const insertAggregation = async ( export const updateAggregationRowOperation = async ( conn: Connection, params: { - sourceAccountId: string; - sourceEntityId: string; - path: string; + aggregationId: string; operation: object; }, ): Promise => { @@ -86,9 +88,7 @@ export const updateAggregationRowOperation = async ( update aggregations set operation = ${JSON.stringify(params.operation)} where - source_account_id = ${params.sourceAccountId} - and source_entity_id = ${params.sourceEntityId} - and path = ${params.path} + aggregation_id = ${params.aggregationId} returning *; `); }; @@ -98,14 +98,11 @@ export const updateAggregationRowOperation = async ( */ export const deleteAggregationRow = async ( conn: Connection, - params: { sourceAccountId: string; sourceEntityId: string; path: string }, + params: { aggregationId: string }, ): Promise => { await conn.one(sql` delete from aggregations - where - source_account_id = ${params.sourceAccountId} - and source_entity_id = ${params.sourceEntityId} - and path = ${params.path} + where aggregation_id = ${params.aggregationId} returning * `); }; @@ -113,9 +110,7 @@ export const deleteAggregationRow = async ( export const addSourceEntityVersionIdToAggregation = async ( conn: Connection, params: { - sourceAccountId: string; - sourceEntityId: string; - path: string; + aggregationId: string; newSourceEntityVersionId: string; }, ) => { @@ -125,9 +120,7 @@ export const addSourceEntityVersionIdToAggregation = async ( update aggregations set source_entity_version_ids = array_append(aggregations.source_entity_version_ids, ${params.newSourceEntityVersionId}) where - source_account_id = ${params.sourceAccountId} - and source_entity_id = ${params.sourceEntityId} - and path = ${params.path} + aggregation_id = ${params.aggregationId} and not ${params.newSourceEntityVersionId} = ANY(aggregations.source_entity_version_ids) returning * ) select * from updated order by created_at desc limit 1; diff --git a/packages/hash/api/src/db/postgres/client.ts b/packages/hash/api/src/db/postgres/client.ts index a2af2aafd76..e5639b14130 100644 --- a/packages/hash/api/src/db/postgres/client.ts +++ b/packages/hash/api/src/db/postgres/client.ts @@ -62,7 +62,8 @@ import { createAggregation } from "./aggregation/createAggregation"; import { getEntityAggregations } from "./aggregation/getEntityAggregations"; import { updateAggregationOperation } from "./aggregation/updateAggregationOperation"; import { deleteAggregation } from "./aggregation/deleteAggregation"; -import { getEntityAggregation } from "./aggregation/getEntityAggregation"; +import { getAggregation } from "./aggregation/getAggregation"; +import { getEntityAggregationByPath } from "./aggregation/getEntityAggregationByPath"; import { requireTransaction } from "./util"; import { getEntityIncomingLinks } from "./link/getEntityIncomingLinks"; import { updateLink } from "./link/updateLink"; @@ -443,10 +444,16 @@ export class PostgresClient implements DbClient { return await updateAggregationOperation(this.conn, params); } - async getEntityAggregation( - params: Parameters[0], - ): ReturnType { - return await getEntityAggregation(this.conn, params); + async getAggregation( + params: Parameters[0], + ): ReturnType { + return await getAggregation(this.conn, params); + } + + async getEntityAggregationByPath( + params: Parameters[0], + ): ReturnType { + return await getEntityAggregationByPath(this.conn, params); } async getEntityAggregations( diff --git a/packages/hash/api/src/db/postgres/entity.ts b/packages/hash/api/src/db/postgres/entity.ts index f402ff479db..8a876077aa3 100644 --- a/packages/hash/api/src/db/postgres/entity.ts +++ b/packages/hash/api/src/db/postgres/entity.ts @@ -682,9 +682,7 @@ const addSourceEntityVersionIdToAggregations = async ( params: { entity: DbEntity; omittedAggregations?: { - sourceAccountId: string; - sourceEntityId: string; - path: string; + aggregationId: string; }[]; newSourceEntityVersionId: string; }, @@ -699,22 +697,18 @@ const addSourceEntityVersionIdToAggregations = async ( const isDbAggregationInNextVersion = (aggregation: DbAggregation): boolean => params.omittedAggregations?.find( - ({ path }) => path === aggregation.path, + ({ aggregationId }) => aggregationId === aggregation.aggregationId, ) === undefined; const { newSourceEntityVersionId } = params; await Promise.all( - aggregations - .filter(isDbAggregationInNextVersion) - .map(({ sourceAccountId, sourceEntityId, path }) => - addSourceEntityVersionIdToAggregation(conn, { - sourceAccountId, - sourceEntityId, - path, - newSourceEntityVersionId, - }), - ), + aggregations.filter(isDbAggregationInNextVersion).map(({ aggregationId }) => + addSourceEntityVersionIdToAggregation(conn, { + aggregationId, + newSourceEntityVersionId, + }), + ), ); }; @@ -730,9 +724,7 @@ export const updateVersionedEntity = async ( properties: any; updatedByAccountId: string; omittedAggregations?: { - sourceAccountId: string; - sourceEntityId: string; - path: string; + aggregationId: string; }[]; }, ) => { diff --git a/packages/hash/api/src/db/postgres/index.ts b/packages/hash/api/src/db/postgres/index.ts index fae3bd8cdad..96e3d80bd9e 100644 --- a/packages/hash/api/src/db/postgres/index.ts +++ b/packages/hash/api/src/db/postgres/index.ts @@ -241,10 +241,16 @@ export class PostgresAdapter extends DataSource implements DbAdapter { return this.query((adapter) => adapter.updateAggregationOperation(params)); } - getEntityAggregation( - params: Parameters[0], - ): ReturnType { - return this.query((adapter) => adapter.getEntityAggregation(params)); + getAggregation( + params: Parameters[0], + ): ReturnType { + return this.query((adapter) => adapter.getAggregation(params)); + } + + getEntityAggregationByPath( + params: Parameters[0], + ): ReturnType { + return this.query((adapter) => adapter.getEntityAggregationByPath(params)); } getEntityAggregations( diff --git a/packages/hash/api/src/graphql/resolvers/linkedAggregation/deleteLinkedAggregation.ts b/packages/hash/api/src/graphql/resolvers/linkedAggregation/deleteLinkedAggregation.ts index b39f80c389d..c3a0bf0e322 100644 --- a/packages/hash/api/src/graphql/resolvers/linkedAggregation/deleteLinkedAggregation.ts +++ b/packages/hash/api/src/graphql/resolvers/linkedAggregation/deleteLinkedAggregation.ts @@ -27,7 +27,7 @@ export const deleteLinkedAggregation: Resolver< throw new ApolloError(msg, "NOT_FOUND"); } - const aggregation = await source.getAggregation(client, { + const aggregation = await source.getAggregationByPath(client, { stringifiedPath: path, }); diff --git a/packages/hash/api/src/graphql/resolvers/linkedAggregation/linkedAggregationResults.ts b/packages/hash/api/src/graphql/resolvers/linkedAggregation/linkedAggregationResults.ts index 2d026c20988..f30e5abab75 100644 --- a/packages/hash/api/src/graphql/resolvers/linkedAggregation/linkedAggregationResults.ts +++ b/packages/hash/api/src/graphql/resolvers/linkedAggregation/linkedAggregationResults.ts @@ -22,7 +22,7 @@ export const linkedAggregationResults: Resolver< throw new ApolloError(msg, "NOT_FOUND"); } - const aggregation = await source.getAggregation(dataSources.db, { + const aggregation = await source.getAggregationByPath(dataSources.db, { stringifiedPath: path, }); diff --git a/packages/hash/api/src/graphql/resolvers/linkedAggregation/updateLinkedAggregationOperation.ts b/packages/hash/api/src/graphql/resolvers/linkedAggregation/updateLinkedAggregationOperation.ts index b3632171bd9..a5d9c10aea7 100644 --- a/packages/hash/api/src/graphql/resolvers/linkedAggregation/updateLinkedAggregationOperation.ts +++ b/packages/hash/api/src/graphql/resolvers/linkedAggregation/updateLinkedAggregationOperation.ts @@ -27,7 +27,7 @@ export const updateLinkedAggregationOperation: Resolver< throw new ApolloError(msg, "NOT_FOUND"); } - const aggregation = await source.getAggregation(client, { + const aggregation = await source.getAggregationByPath(client, { stringifiedPath: path, }); diff --git a/packages/hash/api/src/graphql/typeDefs/aggregation.typedef.ts b/packages/hash/api/src/graphql/typeDefs/aggregation.typedef.ts index 256b37d7f65..1f0079d635e 100644 --- a/packages/hash/api/src/graphql/typeDefs/aggregation.typedef.ts +++ b/packages/hash/api/src/graphql/typeDefs/aggregation.typedef.ts @@ -2,6 +2,7 @@ import { gql } from "apollo-server-express"; export const aggregationTypedef = gql` type LinkedAggregation { + aggregationId: ID! sourceAccountId: ID! sourceEntityId: ID! path: String! diff --git a/packages/hash/api/src/model/aggregation.model.ts b/packages/hash/api/src/model/aggregation.model.ts index ee46fa689b3..0434e5ac355 100644 --- a/packages/hash/api/src/model/aggregation.model.ts +++ b/packages/hash/api/src/model/aggregation.model.ts @@ -1,6 +1,6 @@ import jp from "jsonpath"; import { UserInputError } from "apollo-server-errors"; -import { get, orderBy } from "lodash"; +import { get, merge, orderBy } from "lodash"; import { DbClient } from "../db"; import { Entity, @@ -37,6 +37,8 @@ export type CreateAggregationArgs = { }; export type AggregationConstructorArgs = { + aggregationId: string; + stringifiedPath: string; sourceAccountId: string; @@ -57,6 +59,8 @@ const mapDbAggregationToModel = (dbAggregation: DbAggregation) => }); class __Aggregation { + aggregationId: string; + stringifiedPath: string; path: jp.PathComponent[]; @@ -70,6 +74,7 @@ class __Aggregation { createdAt: Date; constructor({ + aggregationId, stringifiedPath, operation, sourceAccountId, @@ -78,6 +83,7 @@ class __Aggregation { createdAt, createdByAccountId, }: AggregationConstructorArgs) { + this.aggregationId = aggregationId; this.stringifiedPath = stringifiedPath; this.path = Link.parseStringifiedPath(stringifiedPath); this.operation = operation; @@ -213,16 +219,16 @@ class __Aggregation { Link.validatePath(stringifiedPath); - /** @todo: check entity type to see if there is an inverse relationship needs to be created */ - const { accountId: sourceAccountId, entityId: sourceEntityId } = source; if ( - await Aggregation.getEntityAggregation(client, { + await Aggregation.getEntityAggregationByPath(client, { source, stringifiedPath, }) ) { + /** @todo: consider supporting multiple aggregations at the same path */ + throw new Error("Cannot create aggregation that already exists"); } @@ -237,7 +243,7 @@ class __Aggregation { return mapDbAggregationToModel(dbAggregation); } - static async getEntityAggregation( + static async getEntityAggregationByPath( client: DbClient, params: { source: Entity; @@ -247,7 +253,7 @@ class __Aggregation { const { source, stringifiedPath } = params; const { accountId: sourceAccountId, entityId: sourceEntityId } = source; - const dbAggregation = await client.getEntityAggregation({ + const dbAggregation = await client.getEntityAggregationByPath({ sourceAccountId, sourceEntityId, sourceEntityVersionId: source.metadata.versioned @@ -259,6 +265,17 @@ class __Aggregation { return dbAggregation ? mapDbAggregationToModel(dbAggregation) : null; } + static async getAggregationById( + client: DbClient, + params: { + aggregationId: string; + }, + ): Promise { + const dbAggregation = await client.getAggregation(params); + + return dbAggregation ? mapDbAggregationToModel(dbAggregation) : null; + } + static async getAllEntityAggregations( client: DbClient, params: { @@ -290,14 +307,13 @@ class __Aggregation { itemsPerPage: params.operation.itemsPerPage ?? 10, pageNumber: params.operation.pageNumber ?? 1, }; - const { sourceEntityVersionIds } = await client.updateAggregationOperation({ - sourceAccountId: this.sourceAccountId, - sourceEntityId: this.sourceEntityId, - path: this.stringifiedPath, + + const updatedDbAggregation = await client.updateAggregationOperation({ + aggregationId: this.aggregationId, operation, }); - this.operation = operation; - this.sourceEntityVersionIds = sourceEntityVersionIds; + + merge(this, mapDbAggregationToModel(updatedDbAggregation)); return this; } @@ -372,10 +388,8 @@ class __Aggregation { ): Promise { const { deletedByAccountId } = params; await client.deleteAggregation({ + aggregationId: this.aggregationId, deletedByAccountId, - sourceAccountId: this.sourceAccountId, - sourceEntityId: this.sourceEntityId, - path: this.stringifiedPath, }); } @@ -383,6 +397,7 @@ class __Aggregation { client: DbClient, ): Promise { return { + aggregationId: this.aggregationId, sourceAccountId: this.sourceAccountId, sourceEntityId: this.sourceEntityId, path: this.stringifiedPath, diff --git a/packages/hash/api/src/model/entity.model.ts b/packages/hash/api/src/model/entity.model.ts index d1d9c670aac..55703ff385a 100644 --- a/packages/hash/api/src/model/entity.model.ts +++ b/packages/hash/api/src/model/entity.model.ts @@ -393,14 +393,14 @@ class __Entity { return aggregations; } - async getAggregation( + async getAggregationByPath( client: DbClient, params: { stringifiedPath: string; }, ) { const { stringifiedPath } = params; - const aggregation = await Aggregation.getEntityAggregation(client, { + const aggregation = await Aggregation.getEntityAggregationByPath(client, { source: this, stringifiedPath, }); diff --git a/packages/hash/datastore/postgres/migration/1649061269485_add-aggregation-id.ts b/packages/hash/datastore/postgres/migration/1649061269485_add-aggregation-id.ts new file mode 100644 index 00000000000..aeea86dc187 --- /dev/null +++ b/packages/hash/datastore/postgres/migration/1649061269485_add-aggregation-id.ts @@ -0,0 +1,59 @@ +import { MigrationBuilder, ColumnDefinitions } from "node-pg-migrate"; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export async function up(pgm: MigrationBuilder): Promise { + /** + * Step 1. add the `aggregation_id` column if it doesn't already exist. + */ + pgm.addColumn( + "aggregations", + { + aggregation_id: { + type: "uuid", + unique: true, + }, + }, + { + ifNotExists: true, + }, + ); + + /** + * Step 2. genereate UUID values for each existing aggregation. + */ + pgm.sql(` + update aggregations + set aggregation_id = gen_random_uuid() + where aggregation_id is null + `); + + /** + * Step 3. make the `aggregation_id` column non-nullable + */ + pgm.alterColumn("aggregations", "aggregation_id", { + notNull: true, + }); + + /** + * Step 4. replace existing primary key for the `aggregations` table + */ + pgm.dropConstraint("aggregations", "aggregations_pkey", { ifExists: true }); + pgm.addConstraint("aggregations", "aggregations_pkey", { + primaryKey: ["source_account_id", "aggregation_id"], + }); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropConstraint("aggregations", "aggregations_pkey", { ifExists: true }); + pgm.addConstraint("aggregations", "aggregations_pkey", { + primaryKey: [ + "source_account_id", + "source_entity_id", + "path", + "source_entity_version_ids", + ], + }); + + pgm.dropColumn("aggregations", "aggregation_id"); +} diff --git a/packages/hash/datastore/postgres/scripts/generate-ids.ts b/packages/hash/datastore/postgres/scripts/generate-ids.ts index fb17a786878..04f2e7600ec 100644 --- a/packages/hash/datastore/postgres/scripts/generate-ids.ts +++ b/packages/hash/datastore/postgres/scripts/generate-ids.ts @@ -1,16 +1,13 @@ import fs from "fs"; import path from "path"; -import { Uuid4 } from "id128"; import { SYSTEM_TYPES } from "@hashintel/hash-api/src/types/entityTypes"; +import { genId } from "../util"; const requiredIds = { orgs: ["__system__"], types: SYSTEM_TYPES, }; -/** @todo replace this when implementation in the backend/src/util changes */ -const genId = () => Uuid4.generate().toCanonical().toLowerCase(); - export const generatedIds: { [key: string]: { [key: string]: { diff --git a/packages/hash/datastore/postgres/util.ts b/packages/hash/datastore/postgres/util.ts index 50ba326d338..15e85794353 100644 --- a/packages/hash/datastore/postgres/util.ts +++ b/packages/hash/datastore/postgres/util.ts @@ -1,2 +1,31 @@ +import { Uuid4 } from "id128"; +import { MigrationBuilder } from "node-pg-migrate"; + +/** + * @param db + * @param params.tableName the name of the column's table + * @param params.columnName the name of the column + * @returns `true` if the column exists in the table, otherwise `false` + */ +export const columnDoesNotExists = async ( + db: MigrationBuilder["db"], + params: { + tableName: string; + columnName: string; + }, +): Promise => { + const { rows } = await db.query(` + select * + from INFORMATION_SCHEMA.COLUMNS + where table_name = '${params.tableName}' + and column_name = '${params.columnName}' + `); + + return rows.length === 0; +}; + export const stripNewLines = (inputString: string) => inputString.replace(/(\r\n|\n|\r)( *)/gm, " "); + +/** @todo replace this when implementation in the backend/src/util changes */ +export const genId = () => Uuid4.generate().toCanonical().toLowerCase(); diff --git a/packages/hash/integration/src/tests/integration.test.ts b/packages/hash/integration/src/tests/integration.test.ts index 67f2651dc43..508ded9ae47 100644 --- a/packages/hash/integration/src/tests/integration.test.ts +++ b/packages/hash/integration/src/tests/integration.test.ts @@ -1641,7 +1641,7 @@ describe("logged in user ", () => { }); expect(gqlAggregation.results).toHaveLength(numberOfAggregateEntities); - const aggregation = (await sourceEntity.getAggregation(db, { + const aggregation = (await sourceEntity.getAggregationByPath(db, { stringifiedPath: variables.path, }))!; @@ -1725,7 +1725,7 @@ describe("logged in user ", () => { pageCount: 0, }); - const aggregation = (await sourceEntity.getAggregation(db, { + const aggregation = (await sourceEntity.getAggregationByPath(db, { stringifiedPath, }))!; @@ -1760,7 +1760,7 @@ describe("logged in user ", () => { path: stringifiedPath, }); - const aggregation = await sourceEntity.getAggregation(db, { + const aggregation = await sourceEntity.getAggregationByPath(db, { stringifiedPath, });