diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts index aee70a2a..404733b3 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts @@ -6,7 +6,7 @@ /* eslint-disable no-underscore-dangle */ import { Logger, Config } from '@edfi/meadowlark-utilities'; -import { DeleteResult, DeleteRequest, BlockingDocument, DocumentUuid, TraceId } from '@edfi/meadowlark-core'; +import { DeleteResult, DeleteRequest, ReferringDocumentInfo, DocumentUuid, TraceId } from '@edfi/meadowlark-core'; import { ClientSession, Collection, MongoClient, WithId } from 'mongodb'; import retry from 'async-retry'; import { MeadowlarkDocument } from '../model/MeadowlarkDocument'; @@ -55,7 +55,7 @@ async function checkForReferencesToDocument( .find(onlyDocumentsReferencing(deleteCandidate.aliasMeadowlarkIds), limitFive(session)) .toArray(); - const blockingDocuments: BlockingDocument[] = referringDocuments.map((document) => ({ + const referringDocumentInfo: ReferringDocumentInfo[] = referringDocuments.map((document) => ({ documentUuid: document.documentUuid, meadowlarkId: document._id, resourceName: document.resourceName, @@ -63,7 +63,7 @@ async function checkForReferencesToDocument( resourceVersion: document.resourceVersion, })); - return { response: 'DELETE_FAILURE_REFERENCE', blockingDocuments }; + return { response: 'DELETE_FAILURE_REFERENCE', referringDocumentInfo }; } /** diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts index cae6c3e2..ba2d7747 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts @@ -5,7 +5,7 @@ /* eslint-disable no-underscore-dangle */ -import { UpdateResult, UpdateRequest, BlockingDocument } from '@edfi/meadowlark-core'; +import { UpdateResult, UpdateRequest, ReferringDocumentInfo } from '@edfi/meadowlark-core'; import { Logger, Config } from '@edfi/meadowlark-utilities'; import { Collection, ClientSession, MongoClient, WithId } from 'mongodb'; import retry from 'async-retry'; @@ -62,7 +62,7 @@ async function insertUpdatedDocument( traceId, 'Got "INSERT_FAILURE_REFERENCE" from upsertDocumentTransaction() but references should not have been validated', ); - return { response: 'UPDATE_FAILURE_REFERENCE', blockingDocuments: upsertResult.blockingDocuments }; + return { response: 'UPDATE_FAILURE_REFERENCE', referringDocumentInfo: upsertResult.referringDocumentInfo }; case 'UPDATE_FAILURE_REFERENCE': // Something unexpected happened. There should have been a prior delete, and validation should not have occurred. Logger.error( @@ -70,9 +70,9 @@ async function insertUpdatedDocument( traceId, 'Got "UPDATE_FAILURE_REFERENCE" from upsertDocumentTransaction() but document should have been deleted first and references should not have been validated', ); - return { response: 'UPDATE_FAILURE_REFERENCE', blockingDocuments: upsertResult.blockingDocuments }; + return { response: 'UPDATE_FAILURE_REFERENCE', referringDocumentInfo: upsertResult.referringDocumentInfo }; case 'INSERT_FAILURE_CONFLICT': - return { response: 'UPDATE_FAILURE_CONFLICT', blockingDocuments: upsertResult.blockingDocuments }; + return { response: 'UPDATE_FAILURE_CONFLICT', referringDocumentInfo: upsertResult.referringDocumentInfo }; default: return { response: 'UNKNOWN_FAILURE', failureMessage: upsertResult.failureMessage }; } @@ -261,7 +261,7 @@ async function checkForInvalidReferences( .find(onlyDocumentsReferencing([meadowlarkId]), limitFive(session)) .toArray(); - const blockingDocuments: BlockingDocument[] = referringDocuments.map((document) => ({ + const referringDocumentInfo: ReferringDocumentInfo[] = referringDocuments.map((document) => ({ documentUuid: document.documentUuid, meadowlarkId: document._id, resourceName: document.resourceName, @@ -272,7 +272,7 @@ async function checkForInvalidReferences( return { response: 'UPDATE_FAILURE_REFERENCE', failureMessage: { message: 'Reference validation failed', failures }, - blockingDocuments, + referringDocumentInfo, }; } diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts index f8f1290c..91c5eb77 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts @@ -10,7 +10,7 @@ import { UpsertResult, UpsertRequest, getMeadowlarkIdForSuperclassInfo, - BlockingDocument, + ReferringDocumentInfo, DocumentUuid, generateDocumentUuid, MeadowlarkId, @@ -63,7 +63,7 @@ export async function upsertDocumentTransaction( return { response: 'INSERT_FAILURE_CONFLICT', failureMessage: `Insert failed: the identity is in use by '${resourceInfo.resourceName}' which is also a(n) '${documentInfo.superclassInfo.resourceName}'`, - blockingDocuments: [ + referringDocumentInfo: [ { documentUuid: superclassAliasMeadowlarkIdInUse.documentUuid, meadowlarkId: superclassAliasMeadowlarkIdInUse._id, @@ -96,7 +96,7 @@ export async function upsertDocumentTransaction( .find(onlyDocumentsReferencing([meadowlarkId]), limitFive(session)) .toArray(); - const blockingDocuments: BlockingDocument[] = referringDocuments.map((document) => ({ + const referringDocumentInfo: ReferringDocumentInfo[] = referringDocuments.map((document) => ({ documentUuid: document.documentUuid, meadowlarkId: document._id, resourceName: document.resourceName, @@ -107,7 +107,7 @@ export async function upsertDocumentTransaction( return { response: isInsert ? 'INSERT_FAILURE_REFERENCE' : 'UPDATE_FAILURE_REFERENCE', failureMessage: { error: { message: 'Reference validation failed', failures } }, - blockingDocuments, + referringDocumentInfo, }; } } diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/model/MeadowlarkDocument.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/model/MeadowlarkDocument.ts new file mode 100644 index 00000000..0bba787c --- /dev/null +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/model/MeadowlarkDocument.ts @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +import { DocumentIdentity, MeadowlarkId, NoDocumentIdentity, DocumentUuid } from '@edfi/meadowlark-core'; + +export interface MeadowlarkDocumentId { + /** + * A string hash derived from the project name, resource name + * and identity of the API document. This field replaces the built-in MongoDB _id. + */ + meadowlark_id: MeadowlarkId; + /** + * The UUID for the document. + */ + document_uuid: DocumentUuid; +} + +export interface MeadowlarkDocument extends MeadowlarkDocumentId { + /** + * The identity elements extracted from the API document. + */ + document_identity: DocumentIdentity; + + /** + * The MetaEd project name the API document resource is defined in e.g. "EdFi" for a data standard entity. + */ + project_name: string; + + /** + * The name of the resource. Typically, this is the same as the corresponding MetaEd entity name. However, + * there are exceptions, for example descriptors have a "Descriptor" suffix on their resource name. + */ + resource_name: string; + + /** + * The resource version as a string. This is the same as the MetaEd project version + * the entity is defined in e.g. "3.3.1-b" for a 3.3b data standard entity. + */ + resource_version: string; + + /** + * The Ed-Fi ODS/API document itself. + */ + edfi_doc: any; + + /** + * True if this document has been reference and descriptor validated. + */ + validated: boolean; + + /** + * True if this document is a descriptor. + */ + is_descriptor: boolean; + + /** + * Creator of this document + */ + created_by: string; +} + +/** + * Creates a new empty newMeadowlarkDocument object + */ +export function newMeadowlarkDocument(): MeadowlarkDocument { + return { + meadowlark_id: undefined as unknown as MeadowlarkId, + document_uuid: undefined as unknown as DocumentUuid, + document_identity: NoDocumentIdentity, + project_name: '', + resource_name: '', + resource_version: '', + edfi_doc: null, + validated: false, + is_descriptor: false, + created_by: '', + }; +} + +/** + * The null object for DocumentInfo + */ +export const NoMeadowlarkDocument = Object.freeze({ + ...newMeadowlarkDocument(), +}); + +export function isMeadowlarkDocumentEmpty(meadowlarkDocument: MeadowlarkDocument): boolean { + const noMeadowlarkDocument = NoMeadowlarkDocument; + return meadowlarkDocument === noMeadowlarkDocument; +} diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/model/MeadowlarkDocumentAndAliasId.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/model/MeadowlarkDocumentAndAliasId.ts new file mode 100644 index 00000000..3a6c791a --- /dev/null +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/model/MeadowlarkDocumentAndAliasId.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +import { MeadowlarkId } from '@edfi/meadowlark-core'; +import { MeadowlarkDocumentId } from './MeadowlarkDocument'; + +export interface MeadowlarkDocumentAndAliasId extends MeadowlarkDocumentId { + /** + * Alias meadowlarkId. + */ + alias_meadowlark_id: MeadowlarkId; +} diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Db.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Db.ts index 671ef847..8964791c 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Db.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Db.ts @@ -6,18 +6,7 @@ import { Pool, Client } from 'pg'; import type { PoolClient } from 'pg'; import { Config, Logger } from '@edfi/meadowlark-utilities'; -import { - createDocumentTableSql, - createDocumentTableUniqueIndexSql, - createReferencesTableCheckingIndexSql, - createReferencesTableDeletingIndexSql, - createReferencesTableSql, - createSchemaSql, - createDatabaseSql, - createAliasesTableSql, - createAliasesTableDocumentIndexSql, - createAliasesTableAliasIndexSql, -} from './SqlHelper'; +import { createDatabase, checkExistsAndCreateTables } from './SqlHelper'; let singletonDbPool: Pool | null = null; @@ -31,27 +20,6 @@ const getDbConfiguration = () => ({ const moduleName = 'postgresql.repository.Db'; -/** - * Checks that the meadowlark schema, document and references tables exist in the database, if not will create them - * @param client The Postgres client for querying - */ -export async function checkExistsAndCreateTables(client: PoolClient) { - try { - await client.query(createSchemaSql); - await client.query(createDocumentTableSql); - await client.query(createDocumentTableUniqueIndexSql); - await client.query(createReferencesTableSql); - await client.query(createReferencesTableCheckingIndexSql); - await client.query(createReferencesTableDeletingIndexSql); - await client.query(createAliasesTableSql); - await client.query(createAliasesTableDocumentIndexSql); - await client.query(createAliasesTableAliasIndexSql); - } catch (e) { - Logger.error(`${moduleName}.checkExistsAndCreateTables error connecting to PostgreSQL`, null, e); - throw e; - } -} - /** * Create a connection pool, check that the database and table structure is in place * or create it if not and then return the pool @@ -92,7 +60,7 @@ export async function createConnectionPoolAndReturnClient(): Promise const client: Client = new Client(getDbConfiguration()); try { await client.connect(); - await client.query(createDatabaseSql(meadowlarkDbName)); + await createDatabase(client, meadowlarkDbName); Logger.info(`${moduleName}.createConnectionPoolAndReturnClient database ${meadowlarkDbName} created successfully`, null); } catch (e) { diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Delete.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Delete.ts index 1634b6ce..74d9b566 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Delete.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Delete.ts @@ -4,16 +4,20 @@ // See the LICENSE and NOTICES files in the project root for more information. import { Logger } from '@edfi/meadowlark-utilities'; -import type { DeleteResult, DeleteRequest, BlockingDocument, MeadowlarkId } from '@edfi/meadowlark-core'; -import type { PoolClient, QueryResult } from 'pg'; +import type { DeleteResult, DeleteRequest, ReferringDocumentInfo, MeadowlarkId } from '@edfi/meadowlark-core'; +import type { PoolClient } from 'pg'; import { - deleteDocumentByDocumentUuIdSql, - deleteOutboundReferencesOfDocumentByMeadowlarkIdSql, - findReferringDocumentInfoForErrorReportingSql, - deleteAliasesForDocumentByMeadowlarkIdSql, - findReferencingMeadowlarkIdsSql, - findAliasMeadowlarkIdsForDocumentByDocumentUuidSql, + deleteDocumentRowByDocumentUuid, + deleteOutboundReferencesOfDocumentByMeadowlarkId, + findReferringDocumentInfoForErrorReporting, + deleteAliasesForDocumentByMeadowlarkId, + findReferencingMeadowlarkIds, + findAliasMeadowlarkIdsForDocumentByDocumentUuid, + beginTransaction, + rollbackTransaction, + commitTransaction, } from './SqlHelper'; +import { MeadowlarkDocumentAndAliasId } from '../model/MeadowlarkDocumentAndAliasId'; const moduleName = 'postgresql.repository.Delete'; @@ -27,43 +31,31 @@ export async function deleteDocumentByDocumentUuid( let meadowlarkId: MeadowlarkId = '' as MeadowlarkId; try { - await client.query('BEGIN'); + await beginTransaction(client); // Find the alias meadowlarkIds for the document to be deleted - const documentAliasIdsResult: QueryResult = await client.query( - findAliasMeadowlarkIdsForDocumentByDocumentUuidSql(documentUuid), + const documentAliasIdsResult: MeadowlarkDocumentAndAliasId[] = await findAliasMeadowlarkIdsForDocumentByDocumentUuid( + client, + documentUuid, ); // Each row contains documentUuid and corresponding meadowlarkId (meadowlark_id), // we just need the first row to return the meadowlark_id - meadowlarkId = - documentAliasIdsResult?.rowCount != null && documentAliasIdsResult.rowCount > 0 - ? documentAliasIdsResult.rows[0].meadowlark_id - : ('' as MeadowlarkId); + meadowlarkId = documentAliasIdsResult.length > 0 ? documentAliasIdsResult[0].meadowlark_id : ('' as MeadowlarkId); if (validateNoReferencesToDocument) { // All documents have alias meadowlarkIds. If no alias meadowlarkIds were found, the document doesn't exist if (meadowlarkId === '') { - await client.query('ROLLBACK'); + await rollbackTransaction(client); Logger.debug(`${moduleName}.deleteDocumentByDocumentUuid: DocumentUuid ${documentUuid} does not exist`, traceId); deleteResult = { response: 'DELETE_FAILURE_NOT_EXISTS' }; return deleteResult; } // Extract from the query result - const documentAliasMeadowlarkIds: MeadowlarkId[] = documentAliasIdsResult.rows.map((ref) => ref.alias_meadowlark_id); + const documentAliasMeadowlarkIds: MeadowlarkId[] = documentAliasIdsResult.map((ref) => ref.alias_meadowlark_id); // Find any documents that reference this document, either it's own meadowlarkId or an alias - const referenceResult: QueryResult = await client.query( - findReferencingMeadowlarkIdsSql(documentAliasMeadowlarkIds), - ); - - if (referenceResult.rows == null) { - await client.query('ROLLBACK'); - const errorMessage = `${moduleName}.deleteDocumentByDocumentUuid: Error determining documents referenced by ${documentUuid}, a null result set was returned`; - deleteResult.failureMessage = errorMessage; - Logger.error(errorMessage, traceId); - return deleteResult; - } + const referenceResult: MeadowlarkId[] = await findReferencingMeadowlarkIds(client, documentAliasMeadowlarkIds); - const references = referenceResult?.rows.filter((ref) => ref.meadowlark_id !== meadowlarkId); + const references = referenceResult.filter((ref) => ref !== meadowlarkId); // If this document is referenced, it's a validation failure if (references.length > 0) { @@ -73,65 +65,51 @@ export async function deleteDocumentByDocumentUuid( ); // Get the information of up to five referring documents for failure message purposes - const referenceIds = references.map((ref) => ref.parent_meadowlark_id); - const referringDocuments = await client.query(findReferringDocumentInfoForErrorReportingSql(referenceIds)); + const referenceIds = references.map((ref) => ref); + const referringDocumentInfo: ReferringDocumentInfo[] = await findReferringDocumentInfoForErrorReporting( + client, + referenceIds, + ); - if (referringDocuments.rows == null) { - await client.query('ROLLBACK'); + if (referringDocumentInfo.length === 0) { + await rollbackTransaction(client); const errorMessage = `${moduleName}.deleteDocumentByDocumentUuid Error retrieving documents referenced by ${meadowlarkId}, a null result set was returned`; deleteResult.failureMessage = errorMessage; Logger.error(errorMessage, traceId); return deleteResult; } - const blockingDocuments: BlockingDocument[] = referringDocuments.rows.map((document) => ({ - documentUuid: document.documentUuid, - meadowlarkId: document.meadowlark_id, - resourceName: document.resource_name, - projectName: document.project_name, - resourceVersion: document.resource_version, - })); - deleteResult = { response: 'DELETE_FAILURE_REFERENCE', - blockingDocuments, + referringDocumentInfo, }; - await client.query('ROLLBACK'); + await rollbackTransaction(client); return deleteResult; } } // Perform the document delete Logger.debug(`${moduleName}.deleteDocumentByDocumentUuid: Deleting document documentUuid ${documentUuid}`, traceId); - const deleteQueryResult: QueryResult = await client.query(deleteDocumentByDocumentUuIdSql(documentUuid)); - - if (deleteQueryResult.rowCount === 0 || deleteQueryResult.rows == null) { - await client.query('ROLLBACK'); - deleteResult.failureMessage = `deleteDocumentByDocumentUuid: Failure deleting document ${documentUuid}, a null result was returned`; - return deleteResult; - } - - deleteResult = - deleteQueryResult.rows[0].count === '0' ? { response: 'DELETE_FAILURE_NOT_EXISTS' } : { response: 'DELETE_SUCCESS' }; + deleteResult = await deleteDocumentRowByDocumentUuid(client, documentUuid); // Delete references where this is the parent document Logger.debug( `${moduleName}.deleteDocumentByDocumentUuid Deleting references with documentUuid ${documentUuid} as parent meadowlarkId`, traceId, ); - await client.query(deleteOutboundReferencesOfDocumentByMeadowlarkIdSql(meadowlarkId)); + await deleteOutboundReferencesOfDocumentByMeadowlarkId(client, meadowlarkId); // Delete this document from the aliases table Logger.debug( `${moduleName}.deleteDocumentByDocumentUuid Deleting alias entries with meadowlarkId ${meadowlarkId}`, traceId, ); - await client.query(deleteAliasesForDocumentByMeadowlarkIdSql(meadowlarkId)); + await deleteAliasesForDocumentByMeadowlarkId(client, meadowlarkId); - await client.query('COMMIT'); + await commitTransaction(client); } catch (e) { Logger.error(`${moduleName}.deleteDocumentByDocumentUuid`, traceId, e); - await client.query('ROLLBACK'); + await rollbackTransaction(client); return { response: 'UNKNOWN_FAILURE', failureMessage: '' }; } return deleteResult; diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Get.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Get.ts index 16300cd8..16b60eff 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Get.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Get.ts @@ -3,10 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import type { PoolClient, QueryResult } from 'pg'; +import type { PoolClient } from 'pg'; import { GetResult, GetRequest } from '@edfi/meadowlark-core'; import { Logger } from '@edfi/meadowlark-utilities'; -import { findDocumentByDocumentUuidSql } from './SqlHelper'; +import { findDocumentByDocumentUuid } from './SqlHelper'; +import { MeadowlarkDocument, isMeadowlarkDocumentEmpty } from '../model/MeadowlarkDocument'; const moduleName = 'postgresql.repository.Get'; @@ -16,25 +17,13 @@ export async function getDocumentByDocumentUuid( ): Promise { try { Logger.debug(`${moduleName}.getDocumentByDocumentUuid ${documentUuid}`, traceId); - const queryResult: QueryResult = await client.query(findDocumentByDocumentUuidSql(documentUuid)); + const meadowlarkDocument: MeadowlarkDocument = await findDocumentByDocumentUuid(client, documentUuid); - // Postgres will return an empty row set if no results are returned, if we have no rows, there is a problem - if (queryResult.rows == null) { - const errorMessage = `Could not retrieve DocumentUuid ${documentUuid} - a null result set was returned, indicating system failure`; - Logger.error(errorMessage, traceId); - return { - response: 'UNKNOWN_FAILURE', - document: {}, - failureMessage: errorMessage, - }; - } - - if (queryResult.rowCount === 0) return { response: 'GET_FAILURE_NOT_EXISTS', document: {} }; + if (isMeadowlarkDocumentEmpty(meadowlarkDocument)) return { response: 'GET_FAILURE_NOT_EXISTS', document: {} }; const response: GetResult = { response: 'GET_SUCCESS', - document: { id: queryResult.rows[0].meadowlark_id, ...queryResult.rows[0].edfi_doc }, + document: { id: meadowlarkDocument.meadowlark_id, ...meadowlarkDocument.edfi_doc }, }; return response; } catch (e) { diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/OwnershipSecurity.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/OwnershipSecurity.ts index 1e08ce77..a49a943c 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/OwnershipSecurity.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/OwnershipSecurity.ts @@ -5,9 +5,10 @@ import { meadowlarkIdForDocumentIdentity, FrontendRequest, writeRequestToLog, MeadowlarkId } from '@edfi/meadowlark-core'; import { Logger } from '@edfi/meadowlark-utilities'; -import type { PoolClient, QueryResult } from 'pg'; +import type { PoolClient } from 'pg'; import { SecurityResult } from '../security/SecurityResult'; -import { findOwnershipForDocumentByDocumentUuidSql, findOwnershipForDocumentByMeadowlarkIdSql } from './SqlHelper'; +import { findOwnershipForDocumentByDocumentUuid, findOwnershipForDocumentByMeadowlarkId } from './SqlHelper'; +import { MeadowlarkDocument, isMeadowlarkDocumentEmpty } from '../model/MeadowlarkDocument'; function extractIdIfUpsert(frontendRequest: FrontendRequest): MeadowlarkId | undefined { if (frontendRequest.action !== 'upsert') return undefined; @@ -55,23 +56,23 @@ export async function rejectByOwnershipSecurity( } try { - const result: QueryResult = + const meadowlarkDocument: MeadowlarkDocument = documentUuid != null - ? await client.query(findOwnershipForDocumentByDocumentUuidSql(documentUuid)) - : await client.query(findOwnershipForDocumentByMeadowlarkIdSql(meadowlarkId)); + ? await findOwnershipForDocumentByDocumentUuid(client, documentUuid) + : await findOwnershipForDocumentByMeadowlarkId(client, meadowlarkId); - if (result.rows == null) { + if (meadowlarkDocument == null) { Logger.error(`${functionName} Unknown Error determining access`, frontendRequest.traceId); return 'UNKNOWN_FAILURE'; } - if (result.rowCount === 0) { + if (isMeadowlarkDocumentEmpty(meadowlarkDocument)) { Logger.debug(`${functionName} document not found for ${idLogMessage}`, frontendRequest.traceId); return 'NOT_APPLICABLE'; } const { clientId } = frontendRequest.middleware.security; - if (result.rows[0].created_by === clientId) { + if (meadowlarkDocument.created_by === clientId) { Logger.debug(`${functionName} access approved: ${idLogMessage}, clientId ${clientId}`, frontendRequest.traceId); return 'ACCESS_APPROVED'; } diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/ReferenceValidation.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/ReferenceValidation.ts index 41e51740..05712e6e 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/ReferenceValidation.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/ReferenceValidation.ts @@ -12,7 +12,7 @@ import { } from '@edfi/meadowlark-core'; import { Logger } from '@edfi/meadowlark-utilities'; import { PoolClient } from 'pg'; -import { validateReferenceExistenceSql } from './SqlHelper'; +import { validateReferenceExistence } from './SqlHelper'; const moduleName = 'postgresql.repository.ReferenceValidation'; @@ -34,16 +34,14 @@ async function findReferencedMeadowlarkIdsByMeadowlarkId( return []; } - const referenceExistenceResult = await client.query( - validateReferenceExistenceSql(referenceMeadowlarkIds as MeadowlarkId[]), - ); + const referenceExistenceResult = await validateReferenceExistence(client, referenceMeadowlarkIds as MeadowlarkId[]); - if (referenceExistenceResult.rows == null) { + if (referenceExistenceResult == null) { Logger.error(`${moduleName}.findReferencedMeadowlarkIdsByMeadowlarkId Database error parsing references`, traceId); throw new Error(`${moduleName}.findReferencedMeadowlarkIdsByMeadowlarkId Database error parsing references`); } - return referenceExistenceResult.rows.map((val) => val.alias_meadowlark_id); + return referenceExistenceResult.map((val) => val); } /** diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/SqlHelper.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/SqlHelper.ts index 6f9200e4..59929167 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/SqlHelper.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/SqlHelper.ts @@ -2,205 +2,338 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import { DocumentUuid, MeadowlarkId } from '@edfi/meadowlark-core'; +import { ReferringDocumentInfo, DocumentUuid, MeadowlarkId, DeleteResult } from '@edfi/meadowlark-core'; +import { Logger } from '@edfi/meadowlark-utilities'; +import { Client, PoolClient, QueryResult } from 'pg'; import format from 'pg-format'; +import { MeadowlarkDocument, NoMeadowlarkDocument } from '../model/MeadowlarkDocument'; +import { MeadowlarkDocumentAndAliasId } from '../model/MeadowlarkDocumentAndAliasId'; -// SQL for Selects +const moduleName = 'postgresql.repository.SqlHelper'; +/** + * Executes a database begin transaction + * @param client database connector client. + */ +export async function beginTransaction(client: PoolClient) { + await client.query('BEGIN'); +} + +/** + * Executes a database rollback transaction + * @param client database connector client. + */ +export async function rollbackTransaction(client: PoolClient) { + await client.query('ROLLBACK'); +} + +/** + * Executes a database commit transaction + * @param client database connector client. + */ +export async function commitTransaction(client: PoolClient) { + await client.query('COMMIT'); +} /** - * Returns the SQL query that does a reverse lookup to find the meadowlarkIds of documents that reference the given - * meadowlarkIds. + * Checks if a QueryResult has rows. + * @param client database connector client. + * @returns true if it has rows or false if not. + */ +function hasResults(queryResult: QueryResult): boolean { + if (queryResult == null) { + return false; + } + return (queryResult?.rowCount ?? 0) > 0; +} + +/** + * Returns a list of meadowlarkIds of documents that reference the given meadowlarkIds. * - * @param referencedMeadowlarkIds the meadowlarkIds of the documents that are being referenced - * @returns a SQL query to find the meadowlarkIds of the documents referencing the given meadowlarkIds + * @param client database connector client. + * @param referencedMeadowlarkIds the array of meadowlarkIds of the documents that are being referenced + * @returns an array of meadowlarkIds of the documents referencing the given meadowlarkIds */ -export function findReferencingMeadowlarkIdsSql(referencedMeadowlarkIds: MeadowlarkId[]): string { - return format( +export async function findReferencingMeadowlarkIds( + client: PoolClient, + referencedMeadowlarkIds: MeadowlarkId[], +): Promise { + const querySelect = format( `SELECT parent_meadowlark_id FROM meadowlark.references WHERE referenced_meadowlark_id IN (%L)`, referencedMeadowlarkIds, ); + const queryResult: QueryResult = await client.query(querySelect); + return (hasResults(queryResult) ? queryResult.rows.map((ref) => ref.parent_meadowlark_id) : []) as MeadowlarkId[]; } /** - * Returns the SQL query to find the alias meadowlarkIds for a given document. Alias meadowlarkIds include the document - * meadowlarkId itself along with any superclass variations that the document satisifies. For example, a School is a subclass + * Returns the alias meadowlarkIds for a given document. Alias meadowlarkIds include the document + * meadowlarkId itself along with any superclass variations that the document satisfies. For example, a School is a subclass * of EducationOrganization, so each School document has an additional alias meadowlarkId as an EducationOrganization. * * This has a secondary function of read-for-write locking this row for proper updating and deleting. * + * @param client database connector client. * @param meadowlarkId the meadowlarkId of the document to find aliases for - * @returns a SQL query to find the alias meadowlarkIds of the given document + * @returns an array of alias meadowlarkIds of the given document */ -export function findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql(meadowlarkId: MeadowlarkId): string { - return format( +export async function findAliasMeadowlarkIdsForDocumentByMeadowlarkId( + client: PoolClient, + meadowlarkId: MeadowlarkId, +): Promise { + const querySelect = format( `SELECT alias_meadowlark_id FROM meadowlark.aliases WHERE meadowlark_id = %L FOR SHARE NOWAIT`, meadowlarkId, ); + const queryResult: QueryResult = await client.query(querySelect); + return (hasResults(queryResult) ? queryResult.rows.map((ref) => ref.alias_meadowlark_id) : []) as MeadowlarkId[]; } /** - * Returns the SQL query to find the alias meadowlarkIds for a given document. Alias meadowlarkIds include the document - * meadowlarkId itself along with any superclass variations that the document satisifies. For example, a School is a subclass + * Returns the alias meadowlarkIds for a given document. Alias meadowlarkIds include the document + * meadowlarkId itself along with any superclass variations that the document satisfies. For example, a School is a subclass * of EducationOrganization, so each School document has an additional alias meadowlarkId as an EducationOrganization. * * This has a secondary function of read-for-write locking this row for proper updating and deleting. * - * @param meadowlarkId the meadowlarkId of the document to find aliases for - * @returns a SQL query to find the alias meadowlarkIds of the given document + * @param client database connector client. + * @param documentUuid the documentUuid of the document to find aliases for + * @returns an array of alias meadowlarkIds of the given document */ -export function findAliasMeadowlarkIdsForDocumentByDocumentUuidSql(documentUuid: DocumentUuid): string { - return format( +export async function findAliasMeadowlarkIdsForDocumentByDocumentUuid( + client: PoolClient, + documentUuid: DocumentUuid, +): Promise { + const querySelect = format( `SELECT alias_meadowlark_id, meadowlark_id FROM meadowlark.aliases WHERE document_uuid = %L FOR SHARE NOWAIT`, documentUuid, ); + const queryResult: QueryResult = await client.query(querySelect); + return ( + hasResults(queryResult) + ? queryResult.rows.map((ref) => ({ + alias_meadowlark_id: ref.alias_meadowlark_id, + meadowlark_id: ref.meadowlark_id, + })) + : [] + ) as MeadowlarkDocumentAndAliasId[]; } - /** - * Returns the SQL query to find the existence of an alias meadowlarkId. Alias meadowlarkIds include the document meadowlarkId - * itself along with any superclass variations that the document satisifies. For example, a School is a subclass + * Returns a list of alias meadowlarkIds. Alias meadowlarkIds include the document meadowlarkId + * itself along with any superclass variations that the document satisfies. For example, a School is a subclass * of EducationOrganization, so each School document has an additional alias meadowlarkId as an EducationOrganization. * * This function does NOT read-for-write lock. * + * @param client database connector client. * @param meadowlarkId the meadowlarkId of the document to find aliases for - * @returns a SQL query to find the alias meadowlarkIds of the given document + * @returns an array of alias meadowlarkIds of the given document */ -export function findAliasMeadowlarkIdSql(meadowlarkId: MeadowlarkId): string { - return format(`SELECT alias_meadowlark_id FROM meadowlark.aliases WHERE alias_meadowlark_id = %L`, meadowlarkId); +export async function findAliasMeadowlarkId(client: PoolClient, meadowlarkId: MeadowlarkId): Promise { + const querySelect = format( + `SELECT alias_meadowlark_id FROM meadowlark.aliases WHERE alias_meadowlark_id = %L`, + meadowlarkId, + ); + const queryResult: QueryResult = await client.query(querySelect); + return (hasResults(queryResult) ? queryResult.rows.map((ref) => ref.alias_meadowlark_id) : []) as MeadowlarkId[]; } /** - * Returns the SQL statement for retrieving a document (with identity) - * @param meadowlarkId The identifier of the document to retrieve - * @returns SQL query string to retrieve a document + * Returns a document (with identity) + * @param client database connector client. + * @param meadowlarkId The MeadowlarkId of the document to retrieve + * @returns meadowlark document */ -export function findDocumentByMeadowlarkIdSql(meadowlarkId: MeadowlarkId): string { - return format( +export async function findDocumentByMeadowlarkId( + client: PoolClient, + meadowlarkId: MeadowlarkId, +): Promise { + const querySelect = format( ` SELECT document_uuid, meadowlark_id, document_identity, edfi_doc FROM meadowlark.documents WHERE meadowlark_id = %L;`, [meadowlarkId], ); + const queryResult: QueryResult = await client.query(querySelect); + return hasResults(queryResult) ? (queryResult.rows[0] as MeadowlarkDocument) : NoMeadowlarkDocument; } /** - * Returns the SQL statement for retrieving a document (with identity) - * @param meadowlarkId The identifier of the document to retrieve - * @returns SQL query string to retrieve a document + * Returns a document (with identity) + * @param client database connector client. + * @param documentUuid The DocumentUuid of the document to retrieve + * @returns meadowlark document */ -export function findDocumentByDocumentUuidSql(documentUuid: DocumentUuid): string { - return format( +export async function findDocumentByDocumentUuid( + client: PoolClient, + documentUuid: DocumentUuid, +): Promise { + const querySelect = format( ` SELECT document_uuid, meadowlark_id, document_identity, edfi_doc FROM meadowlark.documents WHERE document_uuid = %L;`, [documentUuid], ); + const queryResult: QueryResult = await client.query(querySelect); + return hasResults(queryResult) ? (queryResult.rows[0] as MeadowlarkDocument) : NoMeadowlarkDocument; } /** - * Returns the SQL query for retrieving the ownership for a given document - * @param meadowlarkId The identifier of the document - * @returns SQL query string to retrieve ownership + * Returns the the ownership for a given document + * @param client database connector client. + * @param documentUuid The documentUuid of the document + * @returns a meadowlark document to retrieve ownership */ -export function findOwnershipForDocumentByDocumentUuidSql(documentUuid: DocumentUuid): string { - return format('SELECT created_by FROM meadowlark.documents WHERE document_uuid = %L;', [documentUuid]); +export async function findOwnershipForDocumentByDocumentUuid( + client: PoolClient, + documentUuid: DocumentUuid, +): Promise { + const querySelect = format('SELECT created_by FROM meadowlark.documents WHERE document_uuid = %L;', [documentUuid]); + const queryResult: QueryResult = await client.query(querySelect); + return hasResults(queryResult) ? (queryResult.rows[0] as MeadowlarkDocument) : NoMeadowlarkDocument; } /** - * Returns the SQL query for retrieving the ownership for a given document - * @param meadowlarkId The identifier of the document - * @returns SQL query string to retrieve ownership + * Returns the the ownership for a given document + * @param client database connector client. + * @param meadowlarkId The meadowlarkId of the document + * @returns a meadowlark document to retrieve ownership */ -export function findOwnershipForDocumentByMeadowlarkIdSql(meadowlarkId: MeadowlarkId): string { - return format('SELECT created_by FROM meadowlark.documents WHERE meadowlark_id = %L;', [meadowlarkId]); +export async function findOwnershipForDocumentByMeadowlarkId( + client: PoolClient, + meadowlarkId: MeadowlarkId, +): Promise { + const querySelect = format('SELECT created_by FROM meadowlark.documents WHERE meadowlark_id = %L;', [meadowlarkId]); + const queryResult: QueryResult = await client.query(querySelect); + return hasResults(queryResult) ? (queryResult.rows[0] as MeadowlarkDocument) : NoMeadowlarkDocument; } /** - * Returns the SQL statement for retrieving alias meadowlarkIds from the aliases table for the given + * Returns the alias meadowlarkIds from the aliases table for the given * documents. This allows for checking for the existence of documents from the aliases table for deletes * and general reference validation. * - * @param meadowlarkIds the meadowlarkId of the document we're checking references for - * @returns SQL query string to retrieve existing alias meadowlarkIds + * @param client database connector client. + * @param meadowlarkIds the array of meadowlarkId of the document we're checking references for + * @returns an array of alias meadowlarkIds */ -export function validateReferenceExistenceSql(meadowlarkIds: MeadowlarkId[]): string { - return format( +export async function validateReferenceExistence( + client: PoolClient, + meadowlarkIds: MeadowlarkId[], +): Promise { + const querySelect = format( `SELECT alias_meadowlark_id FROM meadowlark.aliases WHERE alias_meadowlark_id IN (%L) FOR NO KEY UPDATE NOWAIT`, meadowlarkIds, ); + const queryResult: QueryResult = await client.query(querySelect); + return (hasResults(queryResult) ? queryResult.rows.map((ref) => ref.alias_meadowlark_id) : []) as MeadowlarkId[]; } /** - * Returns the SQL statement for retrieving alias meadowlarkIds from the aliases table for the given + * Returns a list alias meadowlarkIds from the aliases table for the given * documents. This allows for checking for the existence of documents from the aliases table for deletes * and general reference validation. * - * @param meadowlarkIds the meadowlarkId of the document we're checking references for + * @param client database connector client. + * @param documentUuids the DocumentUuid array of the document we're checking references for * @returns SQL query string to retrieve existing alias meadowlarkIds */ -export function validateReferenceExistenceByDocumentUuidSql(documentUuids: DocumentUuid[]): string { - return format( +export async function validateReferenceExistenceByDocumentUuid( + client: PoolClient, + documentUuids: DocumentUuid[], +): Promise { + const querySelect = format( `SELECT alias_meadowlark_id FROM meadowlark.aliases WHERE document_uuid IN (%L) FOR NO KEY UPDATE NOWAIT`, documentUuids, ); + const queryResult: QueryResult = await client.query(querySelect); + return (hasResults(queryResult) ? queryResult.rows.map((ref) => ref.alias_meadowlark_id) : []) as MeadowlarkId[]; } /** - * Returns the SQL statement to find up to five documents that reference this document. + * Returns up to five documents that reference this document. * This is for error reporting when an attempt is made to delete the document. * + * @param client database connector client. * @param referringMeadowlarkIds The referring documents - * @returns SQL query string to retrieve the meadowlark_id and resource_name of the referring documents + * @returns an array of the referring documents */ -export function findReferringDocumentInfoForErrorReportingSql(referringMeadowlarkIds: MeadowlarkId[]): string { - return format( +export async function findReferringDocumentInfoForErrorReporting( + client: PoolClient, + referringMeadowlarkIds: MeadowlarkId[], +): Promise { + const querySelect = format( `SELECT project_name, resource_name, resource_version, meadowlark_id, document_uuid FROM meadowlark.documents WHERE meadowlark_id IN (%L) LIMIT 5`, referringMeadowlarkIds, ); + const queryResult: QueryResult = await client.query(querySelect); + return ( + hasResults(queryResult) + ? queryResult.rows.map((document) => ({ + resourceName: document.resource_name, + meadowlarkId: document.meadowlark_id, + documentUuid: document.document_uuid, + projectName: document.project_name, + resourceVersion: document.resource_version, + })) + : [] + ) as ReferringDocumentInfo[]; } -// SQL for inserts/updates/upserts - /** - * Returns the SQL statement to add an alias entry to the alias table + * Inserts an alias entry to the alias table * @param meadowlarkId the document with the given alias meadowlarkId * @param aliasId the alias meadowlarkId for the given document, this may be the same as the meadowlarkId - * @returns SQL query string to insert into the aliases table + * @returns if the result returned rows */ -export function insertAliasSql(documentUuid: DocumentUuid, meadowlarkId: MeadowlarkId, aliasId: MeadowlarkId): string { - return format( +export async function insertAlias( + client: PoolClient, + documentUuid: DocumentUuid, + meadowlarkId: MeadowlarkId, + aliasId: MeadowlarkId, +): Promise { + const queryInsert = format( `INSERT INTO meadowlark.aliases (document_uuid, meadowlark_id, alias_meadowlark_id) VALUES (%L)`, [documentUuid, meadowlarkId, aliasId], ); + const queryResult: QueryResult = await client.query(queryInsert); + + return hasResults(queryResult); } /** - * Returns the SQL statement to insert an outbound reference for a document into the references table + * Inserts an outbound reference for a document into the references table * @param meadowlarkId The parent document of the reference * @param referencedMeadowlarkId The document that is referenced - * @returns SQL query string to insert reference into references table + * @returns if the result returned rows */ -export function insertOutboundReferencesSql(meadowlarkId: MeadowlarkId, referencedMeadowlarkId: MeadowlarkId): string { - return format('INSERT INTO meadowlark.references (parent_meadowlark_id, referenced_meadowlark_id) VALUES (%L);', [ - meadowlarkId, - referencedMeadowlarkId, - ]); +export async function insertOutboundReferences( + client: PoolClient, + meadowlarkId: MeadowlarkId, + referencedMeadowlarkId: MeadowlarkId, +): Promise { + const queryInsert = format( + 'INSERT INTO meadowlark.references (parent_meadowlark_id, referenced_meadowlark_id) VALUES (%L);', + [meadowlarkId, referencedMeadowlarkId], + ); + const queryResult: QueryResult = await client.query(queryInsert); + return hasResults(queryResult); } /** - * Returns the SQL statement for inserting or updating a document in the database + * Inserts or updates a document in the database * @param param0 Document info for insert/update * @param isInsert is insert or update SQL re - * @returns SQL query string for inserting or updating provided document info + * @returns if the result returned rows */ -export function documentInsertOrUpdateSql( +export async function insertOrUpdateDocument( + client: PoolClient, { meadowlarkId, documentUuid, resourceInfo, documentInfo, edfiDoc, validateDocumentReferencesExist, security }, isInsert: boolean, -): string { +): Promise { const documentValues = [ meadowlarkId, documentUuid, @@ -253,68 +386,108 @@ export function documentInsertOrUpdateSql( documentValues[1], ); } - return documentSql; + const queryResult: QueryResult = await client.query(documentSql); + return hasResults(queryResult); } -// SQL for Deletes - +// Deletes +/** + * Checks a delete result to return a deleteResult + * @param deleteQueryResult The result from database + * @param countDeletedRows if true, it validates if at least one row was deleted. + * @returns a DeleteResult + */ +async function checkDeleteResult( + client: PoolClient, + deleteQueryResult: QueryResult, + countDeletedRows: boolean = false, +): Promise { + const deleteResult: DeleteResult = { response: 'UNKNOWN_FAILURE', failureMessage: '' }; + if (!hasResults(deleteQueryResult)) { + await rollbackTransaction(client); + deleteResult.failureMessage = `deleteDocumentByDocumentUuId: Failure deleting document, a null result was returned`; + return deleteResult; + } + if (countDeletedRows) { + return deleteQueryResult.rows[0].count === '0' + ? { response: 'DELETE_FAILURE_NOT_EXISTS' } + : { response: 'DELETE_SUCCESS' }; + } + return { response: 'DELETE_SUCCESS' }; +} /** - * Returns the SQL query for deleting a document from the database + * Deletes a document from the database * @param meadowlarkId the document to delete from the documents table - * @returns SQL query string to delete the document + * @returns a DeleteResult */ -export function deleteDocumentByDocumentUuIdSql(documentUuid: DocumentUuid): string { - return format( +export async function deleteDocumentRowByDocumentUuid( + client: PoolClient, + documentUuid: DocumentUuid, +): Promise { + const deleteQuery = format( 'with del as (DELETE FROM meadowlark.documents WHERE document_uuid = %L RETURNING id) SELECT COUNT(*) FROM del;', [documentUuid], ); + const deleteQueryResult: QueryResult = await client.query(deleteQuery); + return checkDeleteResult(client, deleteQueryResult, true); } /** - * Returns the SQL query for deleting the outbound references of a document from the database. + * Deletes the outbound references of a document from the database. * Used as part of deleting the document itself. * * @param meadowlarkId the meadowlarkId of the document whose outbound references we want to delete - * @returns SQL query string to delete references + * @returns if the result returned rows */ -export function deleteOutboundReferencesOfDocumentByMeadowlarkIdSql(meadowlarkId: MeadowlarkId): string { - const sql = format('DELETE FROM meadowlark.references WHERE parent_meadowlark_id = (%L);', [meadowlarkId]); - return sql; +export async function deleteOutboundReferencesOfDocumentByMeadowlarkId( + client: PoolClient, + meadowlarkId: MeadowlarkId, +): Promise { + const deleteQuery = format('DELETE FROM meadowlark.references WHERE parent_meadowlark_id = (%L);', [meadowlarkId]); + const deleteQueryResult: QueryResult = await client.query(deleteQuery); + return hasResults(deleteQueryResult); } /** - * Returns the SQL query for deleting aliases for a given document meadowlarkId. Used as part of deleting the document itself. + * Deletes aliases for a given document meadowlarkId. Used as part of deleting the document itself. * @param meadowlarkId the meadowlarkId of the document we're deleting aliases for - * @returns SQL query string for deleting aliases + * @returns if the result returned rows */ -export function deleteAliasesForDocumentByMeadowlarkIdSql(meadowlarkId: MeadowlarkId): string { - return format( +export async function deleteAliasesForDocumentByMeadowlarkId( + client: PoolClient, + meadowlarkId: MeadowlarkId, +): Promise { + const deleteQuery = format( `DELETE from meadowlark.aliases - WHERE meadowlark_id = %L`, + WHERE meadowlark_id = %L;`, [meadowlarkId], ); + const deleteQueryResult: QueryResult = await client.query(deleteQuery); + return hasResults(deleteQueryResult); } // SQL for DDL /** - * Returns the SQL to create the meadowlark database + * Creates the meadowlark database * @param meadowlarkDbName the name of the database to create - * @returns SQL query string to create the database + * @returns if the result returned rows */ -export function createDatabaseSql(meadowlarkDbName: string): string { - return format('CREATE DATABASE %I', meadowlarkDbName); +export async function createDatabase(client: Client, meadowlarkDbName: string): Promise { + const createDatabaseScript = format('CREATE DATABASE %I', meadowlarkDbName); + const createDatabaseResult: QueryResult = await client.query(createDatabaseScript); + return hasResults(createDatabaseResult); } /** * SQL query string to create schema in the meadowlark database */ -export const createSchemaSql = 'CREATE SCHEMA IF NOT EXISTS meadowlark'; +const createSchemaSql = 'CREATE SCHEMA IF NOT EXISTS meadowlark'; /** * SQL query string to create document table */ -export const createDocumentTableSql = ` +const createDocumentTableSql = ` CREATE TABLE IF NOT EXISTS meadowlark.documents( id bigserial PRIMARY KEY, meadowlark_id VARCHAR(56) NOT NULL, @@ -329,34 +502,34 @@ export const createDocumentTableSql = ` edfi_doc JSONB NOT NULL);`; // All queries are on meadowlark_id, which must be unique -export const createDocumentTableUniqueIndexSql = +const createDocumentTableUniqueIndexSql = 'CREATE UNIQUE INDEX IF NOT EXISTS ux_meadowlark_documents ON meadowlark.documents(meadowlark_id)'; // All queries are on document_uuid, which must be unique -export const createDocumentUuidTableUniqueIndexSql = +const createDocumentUuidTableUniqueIndexSql = 'CREATE UNIQUE INDEX IF NOT EXISTS ux_meadowlark_document_uuid ON meadowlark.documents(document_uuid)'; /** * SQL query string to create the references table */ -export const createReferencesTableSql = ` +const createReferencesTableSql = ` CREATE TABLE IF NOT EXISTS meadowlark.references( id bigserial PRIMARY KEY, parent_meadowlark_id VARCHAR NOT NULL, referenced_meadowlark_id VARCHAR NOT NULL);`; // For reference checking before parent delete -export const createReferencesTableCheckingIndexSql = +const createReferencesTableCheckingIndexSql = 'CREATE INDEX IF NOT EXISTS ix_meadowlark_references_checking ON meadowlark.references(referenced_meadowlark_id)'; // For reference removal in transaction with parent update/delete -export const createReferencesTableDeletingIndexSql = +const createReferencesTableDeletingIndexSql = 'CREATE INDEX IF NOT EXISTS ix_meadowlark_references_deleting ON meadowlark.references(parent_meadowlark_id)'; /** * SQL query string to create the aliases table */ -export const createAliasesTableSql = ` +const createAliasesTableSql = ` CREATE TABLE IF NOT EXISTS meadowlark.aliases( id bigserial PRIMARY KEY, meadowlark_id VARCHAR, @@ -364,13 +537,36 @@ export const createAliasesTableSql = ` alias_meadowlark_id VARCHAR);`; // For finding alias meadowlarkIds given a document meadowlarkId -export const createAliasesTableDocumentIndexSql = +const createAliasesTableDocumentIndexSql = 'CREATE INDEX IF NOT EXISTS ix_meadowlark_aliases_meadowlark_id ON meadowlark.aliases(meadowlark_id)'; // For finding alias meadowlarkIds given a document_uuid -export const createAliasesTableDocumentUuidIndexSql = +const createAliasesTableDocumentUuidIndexSql = 'CREATE INDEX IF NOT EXISTS ix_meadowlark_aliases_document_uuid ON meadowlark.aliases(document_uuid)'; // For finding document meadowlarkIds given an alias meadowlarkId -export const createAliasesTableAliasIndexSql = +const createAliasesTableAliasIndexSql = 'CREATE INDEX IF NOT EXISTS ix_meadowlark_aliases_alias_meadowlark_id ON meadowlark.aliases(alias_meadowlark_id)'; + +/** + * Checks that the meadowlark schema, document and references tables exist in the database, if not will create them + * @param client The Postgres client for querying + */ +export async function checkExistsAndCreateTables(client: PoolClient) { + try { + await client.query(createSchemaSql); + await client.query(createDocumentTableSql); + await client.query(createDocumentTableUniqueIndexSql); + await client.query(createDocumentUuidTableUniqueIndexSql); + await client.query(createReferencesTableSql); + await client.query(createReferencesTableCheckingIndexSql); + await client.query(createReferencesTableDeletingIndexSql); + await client.query(createAliasesTableSql); + await client.query(createAliasesTableDocumentIndexSql); + await client.query(createAliasesTableDocumentUuidIndexSql); + await client.query(createAliasesTableAliasIndexSql); + } catch (e) { + Logger.error(`${moduleName}.checkExistsAndCreateTables error connecting to PostgreSQL`, null, e); + throw e; + } +} diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Update.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Update.ts index db5469bb..0fd40cda 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Update.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Update.ts @@ -10,20 +10,24 @@ import { DocumentReference, getMeadowlarkIdForDocumentReference, getMeadowlarkIdForSuperclassInfo, - BlockingDocument, + ReferringDocumentInfo, } from '@edfi/meadowlark-core'; import { Logger } from '@edfi/meadowlark-utilities'; -import type { PoolClient, QueryResult } from 'pg'; +import type { PoolClient } from 'pg'; import { - documentInsertOrUpdateSql, - findAliasMeadowlarkIdsForDocumentByDocumentUuidSql, - deleteAliasesForDocumentByMeadowlarkIdSql, - insertAliasSql, - deleteOutboundReferencesOfDocumentByMeadowlarkIdSql, - insertOutboundReferencesSql, - findReferringDocumentInfoForErrorReportingSql, + insertOrUpdateDocument, + findAliasMeadowlarkIdsForDocumentByDocumentUuid, + deleteAliasesForDocumentByMeadowlarkId, + insertAlias, + deleteOutboundReferencesOfDocumentByMeadowlarkId, + insertOutboundReferences, + findReferringDocumentInfoForErrorReporting, + beginTransaction, + rollbackTransaction, + commitTransaction, } from './SqlHelper'; import { validateReferences } from './ReferenceValidation'; +import { MeadowlarkDocumentAndAliasId } from '../model/MeadowlarkDocumentAndAliasId'; const moduleName = 'postgresql.repository.Update'; @@ -46,17 +50,20 @@ export async function updateDocumentByDocumentUuid(updateRequest: UpdateRequest, ); try { - await client.query('BEGIN'); + await beginTransaction(client); - const recordExistsResult = await client.query(findAliasMeadowlarkIdsForDocumentByDocumentUuidSql(documentUuid)); + const recordExistsResult: MeadowlarkDocumentAndAliasId[] = await findAliasMeadowlarkIdsForDocumentByDocumentUuid( + client, + documentUuid, + ); // if this record doesn't exist, this function returns a failure - if ((recordExistsResult?.rowCount ?? 0) === 0) { + if (recordExistsResult.length === 0) { updateResult = { response: 'UPDATE_FAILURE_NOT_EXISTS' }; return updateResult; } // Each row contains documentUuid and corresponding meadowlarkId (meadowlark_id), // we just need the first row to return the meadowlark_id - const existingMeadowlarkId: MeadowlarkId = recordExistsResult.rows[0].meadowlark_id; + const existingMeadowlarkId: MeadowlarkId = recordExistsResult[0].meadowlark_id; if (!resourceInfo.allowIdentityUpdates && existingMeadowlarkId !== meadowlarkId) { updateResult = { response: 'UPDATE_FAILURE_IMMUTABLE_IDENTITY' }; return updateResult; @@ -77,44 +84,36 @@ export async function updateDocumentByDocumentUuid(updateRequest: UpdateRequest, traceId, ); - const referringDocuments = await client.query(findReferringDocumentInfoForErrorReportingSql([existingMeadowlarkId])); - - const blockingDocuments: BlockingDocument[] = referringDocuments.rows.map((document) => ({ - resourceName: document.resource_name, - meadowlarkId: document.meadowlark_id, - documentUuid: document.document_uuid, - projectName: document.project_name, - resourceVersion: document.resource_version, - })); + const referringDocumentInfo: ReferringDocumentInfo[] = await findReferringDocumentInfoForErrorReporting(client, [ + existingMeadowlarkId, + ]); updateResult = { response: 'UPDATE_FAILURE_REFERENCE', failureMessage: { error: { message: 'Reference validation failed', failures } }, - blockingDocuments, + referringDocumentInfo, }; - await client.query('ROLLBACK'); + await rollbackTransaction(client); return updateResult; } } // Perform the document update - const documentSql: string = documentInsertOrUpdateSql( + const insertOrUpdateResult: boolean = await insertOrUpdateDocument( + client, { meadowlarkId, documentUuid, resourceInfo, documentInfo, edfiDoc, validateDocumentReferencesExist, security }, false, ); - const result: QueryResult = await client.query(documentSql); - // Delete existing alias using the old meadowlarkId - const deleteAliasesSql = deleteAliasesForDocumentByMeadowlarkIdSql(existingMeadowlarkId); // Delete existing values from the aliases table - await client.query(deleteAliasesSql); + await deleteAliasesForDocumentByMeadowlarkId(client, existingMeadowlarkId); // Perform insert of alias meadowlarkIds - await client.query(insertAliasSql(documentUuid, meadowlarkId, meadowlarkId)); + await insertAlias(client, documentUuid, meadowlarkId, meadowlarkId); if (documentInfo.superclassInfo != null) { const superclassAliasMeadowlarkId: MeadowlarkId = getMeadowlarkIdForSuperclassInfo( documentInfo.superclassInfo, ) as MeadowlarkId; - await client.query(insertAliasSql(documentUuid, meadowlarkId, superclassAliasMeadowlarkId)); + await insertAlias(client, documentUuid, meadowlarkId, superclassAliasMeadowlarkId); } // Delete existing references in references table (by old meadowlarkId) @@ -122,7 +121,7 @@ export async function updateDocumentByDocumentUuid(updateRequest: UpdateRequest, `${moduleName}.upsertDocument: Deleting references for document meadowlarkId ${existingMeadowlarkId}`, traceId, ); - await client.query(deleteOutboundReferencesOfDocumentByMeadowlarkIdSql(existingMeadowlarkId)); + await deleteOutboundReferencesOfDocumentByMeadowlarkId(client, existingMeadowlarkId); // Adding descriptors to outboundRefs for reference checking const descriptorOutboundRefs = documentInfo.descriptorReferences.map((dr: DocumentReference) => @@ -137,21 +136,20 @@ export async function updateDocumentByDocumentUuid(updateRequest: UpdateRequest, `${moduleName}.upsertDocument: Inserting reference meadowlarkId ${ref} for document meadowlarkId ${meadowlarkId}`, ref, ); - await client.query(insertOutboundReferencesSql(meadowlarkId, ref as MeadowlarkId)); + await insertOutboundReferences(client, meadowlarkId, ref as MeadowlarkId); } - await client.query('COMMIT'); + await commitTransaction(client); - updateResult = - result.rowCount && result.rowCount > 0 - ? { - response: 'UPDATE_SUCCESS', - } - : { - response: 'UPDATE_FAILURE_NOT_EXISTS', - }; + updateResult = insertOrUpdateResult + ? { + response: 'UPDATE_SUCCESS', + } + : { + response: 'UPDATE_FAILURE_NOT_EXISTS', + }; } catch (e) { - await client.query('ROLLBACK'); + await rollbackTransaction(client); Logger.error(`${moduleName}.upsertDocument`, traceId, e); return { response: 'UNKNOWN_FAILURE', failureMessage: e.message }; } diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Upsert.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Upsert.ts index 8c44bf12..0ed9401d 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Upsert.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/src/repository/Upsert.ts @@ -3,30 +3,34 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import type { PoolClient, QueryResult } from 'pg'; +import type { PoolClient } from 'pg'; import { UpsertResult, UpsertRequest, DocumentReference, getMeadowlarkIdForDocumentReference, getMeadowlarkIdForSuperclassInfo, - BlockingDocument, + ReferringDocumentInfo, generateDocumentUuid, DocumentUuid, MeadowlarkId, } from '@edfi/meadowlark-core'; import { Logger } from '@edfi/meadowlark-utilities'; import { - deleteOutboundReferencesOfDocumentByMeadowlarkIdSql, - documentInsertOrUpdateSql, - insertOutboundReferencesSql, - deleteAliasesForDocumentByMeadowlarkIdSql, - insertAliasSql, - findAliasMeadowlarkIdSql, - findReferringDocumentInfoForErrorReportingSql, - findDocumentByMeadowlarkIdSql, + deleteOutboundReferencesOfDocumentByMeadowlarkId, + insertOrUpdateDocument, + insertOutboundReferences, + deleteAliasesForDocumentByMeadowlarkId, + insertAlias, + findAliasMeadowlarkId, + findReferringDocumentInfoForErrorReporting, + findDocumentByMeadowlarkId, + beginTransaction, + rollbackTransaction, + commitTransaction, } from './SqlHelper'; import { validateReferences } from './ReferenceValidation'; +import { MeadowlarkDocument, isMeadowlarkDocumentEmpty } from '../model/MeadowlarkDocument'; const moduleName = 'postgresql.repository.Upsert'; @@ -40,19 +44,21 @@ export async function upsertDocument( getMeadowlarkIdForDocumentReference(dr), ); try { - await client.query('BEGIN'); + await beginTransaction(client); // Check whether this is an insert or update - const documentExistsResult: QueryResult = await client.query(findDocumentByMeadowlarkIdSql(meadowlarkId)); - const isInsert: boolean = documentExistsResult.rowCount === 0; - const documentUuid: DocumentUuid = - documentExistsResult.rowCount > 0 ? documentExistsResult.rows[0].document_uuid : generateDocumentUuid(); + const documentExistsResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, meadowlarkId); + const isInsert: boolean = isMeadowlarkDocumentEmpty(documentExistsResult); + const documentUuid: DocumentUuid = !isMeadowlarkDocumentEmpty(documentExistsResult) + ? documentExistsResult.document_uuid + : generateDocumentUuid(); // If inserting a subclass, check whether the superclass identity is already claimed by a different subclass if (isInsert && documentInfo.superclassInfo != null) { - const superclassAliasMeadowlarkIdInUseResult = await client.query( - findAliasMeadowlarkIdSql(getMeadowlarkIdForSuperclassInfo(documentInfo.superclassInfo) as MeadowlarkId), + const superclassAliasMeadowlarkIdInUseResult: MeadowlarkId[] = await findAliasMeadowlarkId( + client, + getMeadowlarkIdForSuperclassInfo(documentInfo.superclassInfo) as MeadowlarkId, ); - const superclassAliasMeadowlarkIdInUse: boolean = superclassAliasMeadowlarkIdInUseResult.rowCount !== 0; + const superclassAliasMeadowlarkIdInUse: boolean = superclassAliasMeadowlarkIdInUseResult.length !== 0; if (superclassAliasMeadowlarkIdInUse) { Logger.debug( @@ -64,30 +70,19 @@ export async function upsertDocument( documentInfo.superclassInfo, ) as MeadowlarkId; - const referringDocuments = await client.query(findReferringDocumentInfoForErrorReportingSql([superclassAliasId])); - - const blockingDocuments: BlockingDocument[] = referringDocuments.rows.map((document) => ({ - resourceName: document.resource_name, - documentUuid: document.document_uuid, - meadowlarkId: document.meadowlark_id, - projectName: document.project_name, - resourceVersion: document.resource_version, - })); + const referringDocumentInfo: ReferringDocumentInfo[] = await findReferringDocumentInfoForErrorReporting(client, [ + superclassAliasId, + ]); - await client.query('ROLLBACK'); + await rollbackTransaction(client); return { response: 'INSERT_FAILURE_CONFLICT', failureMessage: `Insert failed: the identity is in use by '${resourceInfo.resourceName}' which is also a(n) '${documentInfo.superclassInfo.resourceName}'`, - blockingDocuments, + referringDocumentInfo, }; } } - const documentUpsertSql: string = documentInsertOrUpdateSql( - { meadowlarkId, documentUuid, resourceInfo, documentInfo, edfiDoc, validateDocumentReferencesExist, security }, - isInsert, - ); - if (validateDocumentReferencesExist) { const failures = await validateReferences( documentInfo.documentReferences, @@ -103,40 +98,38 @@ export async function upsertDocument( traceId, ); - const referringDocuments = await client.query(findReferringDocumentInfoForErrorReportingSql([meadowlarkId])); - - const blockingDocuments: BlockingDocument[] = referringDocuments.rows.map((document) => ({ - resourceName: document.resource_name, - documentUuid: document.document_uuid, - meadowlarkId: document.meadowlark_id, - projectName: document.project_name, - resourceVersion: document.resource_version, - })); + const referringDocumentInfo: ReferringDocumentInfo[] = await findReferringDocumentInfoForErrorReporting(client, [ + meadowlarkId, + ]); - await client.query('ROLLBACK'); + await rollbackTransaction(client); return { response: isInsert ? 'INSERT_FAILURE_REFERENCE' : 'UPDATE_FAILURE_REFERENCE', failureMessage: { error: { message: 'Reference validation failed', failures } }, - blockingDocuments, + referringDocumentInfo, }; } } // Perform the document upsert Logger.debug(`${moduleName}.upsertDocument: Upserting document meadowlarkId ${meadowlarkId}`, traceId); - await client.query(documentUpsertSql); + await insertOrUpdateDocument( + client, + { meadowlarkId, documentUuid, resourceInfo, documentInfo, edfiDoc, validateDocumentReferencesExist, security }, + isInsert, + ); // Delete existing values from the aliases table - await client.query(deleteAliasesForDocumentByMeadowlarkIdSql(meadowlarkId)); + await deleteAliasesForDocumentByMeadowlarkId(client, meadowlarkId); // Perform insert of alias ids - await client.query(insertAliasSql(documentUuid, meadowlarkId, meadowlarkId)); + await insertAlias(client, documentUuid, meadowlarkId, meadowlarkId); if (documentInfo.superclassInfo != null) { const superclassAliasId: MeadowlarkId = getMeadowlarkIdForSuperclassInfo(documentInfo.superclassInfo) as MeadowlarkId; - await client.query(insertAliasSql(documentUuid, meadowlarkId, superclassAliasId)); + await insertAlias(client, documentUuid, meadowlarkId, superclassAliasId); } // Delete existing references in references table Logger.debug(`${moduleName}.upsertDocument: Deleting references for document meadowlarkId ${meadowlarkId}`, traceId); - await client.query(deleteOutboundReferencesOfDocumentByMeadowlarkIdSql(meadowlarkId)); + await deleteOutboundReferencesOfDocumentByMeadowlarkId(client, meadowlarkId); // Adding descriptors to outboundRefs for reference checking const descriptorOutboundRefs = documentInfo.descriptorReferences.map((dr: DocumentReference) => @@ -151,16 +144,16 @@ export async function upsertDocument( `post${moduleName}.upsertDocument: Inserting reference meadowlarkId ${ref} for document meadowlarkId ${meadowlarkId}`, ref, ); - await client.query(insertOutboundReferencesSql(meadowlarkId, ref as MeadowlarkId)); + await insertOutboundReferences(client, meadowlarkId, ref as MeadowlarkId); } - await client.query('COMMIT'); + await commitTransaction(client); return isInsert ? { response: 'INSERT_SUCCESS', newDocumentUuid: documentUuid } : { response: 'UPDATE_SUCCESS', existingDocumentUuid: documentUuid }; } catch (e) { Logger.error(`${moduleName}.upsertDocument`, traceId, e); - await client.query('ROLLBACK'); + await rollbackTransaction(client); return { response: 'UNKNOWN_FAILURE', failureMessage: e.message }; } } diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Delete.test.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Delete.test.ts index 56205380..30abafe0 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Delete.test.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Delete.test.ts @@ -30,11 +30,12 @@ import { getSharedClient, resetSharedClient } from '../../src/repository/Db'; import { deleteDocumentByDocumentUuid } from '../../src/repository/Delete'; import { upsertDocument } from '../../src/repository/Upsert'; import { - findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql, - findDocumentByDocumentUuidSql, - findDocumentByMeadowlarkIdSql, - findReferencingMeadowlarkIdsSql, + findAliasMeadowlarkIdsForDocumentByMeadowlarkId, + findDocumentByDocumentUuid, + findDocumentByMeadowlarkId, + findReferencingMeadowlarkIds, } from '../../src/repository/SqlHelper'; +import { MeadowlarkDocument, isMeadowlarkDocumentEmpty } from '../../src/model/MeadowlarkDocument'; const newUpsertRequest = (): UpsertRequest => ({ meadowlarkId: '' as MeadowlarkId, @@ -129,8 +130,8 @@ describe('given the delete of an existing document', () => { }); it('should have deleted the document in the db', async () => { - const result: any = await client.query(findDocumentByDocumentUuidSql(documentUuid)); - expect(result.rowCount).toEqual(0); + const result: MeadowlarkDocument = await findDocumentByDocumentUuid(client, documentUuid); + expect(isMeadowlarkDocumentEmpty(result)).toEqual(true); }); }); @@ -216,8 +217,8 @@ describe('given an delete of a document referenced by an existing document with }); it('should still have the referenced document in the db', async () => { - const docResult: any = await client.query(findDocumentByMeadowlarkIdSql(referencedDocumentId)); - expect(docResult.rows[0].document_identity.natural).toBe('delete5'); + const docResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, referencedDocumentId); + expect(docResult.document_identity.natural).toBe('delete5'); }); }); @@ -305,22 +306,23 @@ describe('given an delete of a document with an outbound reference only, with va }); it('should have deleted the document in the db', async () => { - const result: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesMeadowlarkId); - expect(result.rowCount).toEqual(0); + expect(isMeadowlarkDocumentEmpty(result)).toEqual(true); }); it('should have deleted the document alias in the db', async () => { - const result: any = await client.query( - findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId), + const result: MeadowlarkId[] = await findAliasMeadowlarkIdsForDocumentByMeadowlarkId( + client, + documentWithReferencesMeadowlarkId, ); - expect(result.rowCount).toEqual(0); + expect(result.length).toEqual(0); }); it('should have deleted the document reference in the db', async () => { - const result: any = await client.query(findReferencingMeadowlarkIdsSql([referencedDocumentId])); + const result: MeadowlarkId[] = await findReferencingMeadowlarkIds(client, [referencedDocumentId]); - expect(result.rowCount).toEqual(0); + expect(result.length).toEqual(0); }); }); @@ -407,8 +409,8 @@ describe('given an delete of a document referenced by an existing document with }); it('should not have the referenced document in the db', async () => { - const docResult: any = await client.query(findDocumentByMeadowlarkIdSql(referencedDocumentId)); - expect(docResult.rowCount).toEqual(0); + const docResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, referencedDocumentId); + expect(isMeadowlarkDocumentEmpty(docResult)).toEqual(true); }); it('should not be the parent document in the references table', async () => { @@ -511,7 +513,7 @@ describe('given the delete of a subclass document referenced by an existing docu }); it('should still have the referenced document in the db', async () => { - const result: any = await client.query(findDocumentByMeadowlarkIdSql(referencedDocumentId)); - expect(result.rows[0].document_identity.schoolId).toBe('123'); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, referencedDocumentId); + expect(result.document_identity.schoolId).toBe('123'); }); }); diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Read.test.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Read.test.ts index 215c2dc1..e11c5064 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Read.test.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Read.test.ts @@ -23,8 +23,9 @@ import type { PoolClient } from 'pg'; import { resetSharedClient, getSharedClient } from '../../src/repository/Db'; import { deleteAll } from './TestHelper'; import { getDocumentByDocumentUuid } from '../../src/repository/Get'; -import { findDocumentByMeadowlarkIdSql } from '../../src/repository/SqlHelper'; +import { findDocumentByMeadowlarkId } from '../../src/repository/SqlHelper'; import { upsertDocument } from '../../src/repository/Upsert'; +import { MeadowlarkDocument, isMeadowlarkDocumentEmpty } from '../../src/model/MeadowlarkDocument'; const newGetRequest = (): GetRequest => ({ documentUuid: 'deb6ea15-fa93-4389-89a8-1428fb617490' as DocumentUuid, @@ -71,9 +72,9 @@ describe('given the get of a non-existent document', () => { }); it('should not exist in the db', async () => { - const result = await client.query(findDocumentByMeadowlarkIdSql(meadowlarkId)); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, meadowlarkId); - expect(result.rowCount).toBe(0); + expect(isMeadowlarkDocumentEmpty(result)).toBe(true); }); it('should return get failure', async () => { diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Update.test.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Update.test.ts index 76a67837..2fdf081b 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Update.test.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Update.test.ts @@ -31,10 +31,11 @@ import { upsertDocument } from '../../src/repository/Upsert'; import { deleteAll, retrieveReferencesByMeadowlarkIdSql, verifyAliasMeadowlarkId } from './TestHelper'; import { getDocumentByDocumentUuid } from '../../src/repository/Get'; import { - findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql, - findDocumentByDocumentUuidSql, - findDocumentByMeadowlarkIdSql, + findAliasMeadowlarkIdsForDocumentByMeadowlarkId, + findDocumentByDocumentUuid, + findDocumentByMeadowlarkId, } from '../../src/repository/SqlHelper'; +import { MeadowlarkDocument } from '../../src/model/MeadowlarkDocument'; const documentUuid: DocumentUuid = 'feb82f3e-3685-4868-86cf-f4b91749a799' as DocumentUuid; let resultDocumentUuid: DocumentUuid; @@ -173,11 +174,11 @@ describe('given the update of an existing document', () => { }); it('should have updated the document in the db', async () => { - const result: any = await client.query(findDocumentByDocumentUuidSql(resultDocumentUuid)); + const result: MeadowlarkDocument = await findDocumentByDocumentUuid(client, resultDocumentUuid); - expect(result.rows[0].document_identity.natural).toBe('update2'); + expect(result.document_identity.natural).toBe('update2'); - expect(result.rows[0].edfi_doc.changeToDoc).toBe(true); + expect(result.edfi_doc.changeToDoc).toBe(true); }); }); @@ -248,12 +249,12 @@ describe('given an update of a document that references a non-existent document }); it('should have updated the document with an invalid reference in the db', async () => { - const docResult: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesId)); + const docResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesId); const refsResult: any = await client.query(retrieveReferencesByMeadowlarkIdSql(documentWithReferencesId)); const outboundRefs = refsResult.rows.map((ref) => ref.referenced_meadowlark_id); - expect(docResult.rows[0].document_identity.natural).toBe('update4'); + expect(docResult.document_identity.natural).toBe('update4'); expect(outboundRefs).toMatchInlineSnapshot(` [ @@ -353,11 +354,11 @@ describe('given an update of a document that references an existing document wit }); it('should have updated the document with a valid reference in the db', async () => { - const docResult: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesId)); + const docResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesId); const refsResult: any = await client.query(retrieveReferencesByMeadowlarkIdSql(documentWithReferencesId)); const outboundRefs = refsResult.rows.map((ref) => ref.referenced_meadowlark_id); - expect(docResult.rows[0].document_identity.natural).toBe('update6'); + expect(docResult.document_identity.natural).toBe('update6'); expect(outboundRefs).toMatchInlineSnapshot(` [ "Qw5FvPdKxAXWnGghsMh3I61yLFfls4Q949Fk2w", @@ -750,14 +751,14 @@ describe('given the update of an existing document changing meadowlarkId with al }); it('should have deleted the document alias related to the old meadowlarkId in the db', async () => { - const result: any = await client.query(findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql(meadowlarkId)); + const result: MeadowlarkId[] = await findAliasMeadowlarkIdsForDocumentByMeadowlarkId(client, meadowlarkId); - expect(result.rowCount).toEqual(0); + expect(result.length).toEqual(0); }); it('should have created the document reference related to the new meadowlarkId in the db', async () => { - const result: any = await client.query(findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql(meadowlarkIdUpdated)); + const result: MeadowlarkId[] = await findAliasMeadowlarkIdsForDocumentByMeadowlarkId(client, meadowlarkIdUpdated); - expect(result.rowCount).toEqual(1); + expect(result.length).toEqual(1); }); }); diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Upsert.test.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Upsert.test.ts index 1161a4b6..744abcaa 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Upsert.test.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/Upsert.test.ts @@ -23,12 +23,10 @@ import { } from '@edfi/meadowlark-core'; import type { PoolClient } from 'pg'; import { getSharedClient, resetSharedClient } from '../../src/repository/Db'; -import { - findDocumentByMeadowlarkIdSql, - findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql, -} from '../../src/repository/SqlHelper'; +import { findDocumentByMeadowlarkId, findAliasMeadowlarkIdsForDocumentByMeadowlarkId } from '../../src/repository/SqlHelper'; import { upsertDocument } from '../../src/repository/Upsert'; import { deleteAll, retrieveReferencesByMeadowlarkIdSql, verifyAliasMeadowlarkId } from './TestHelper'; +import { isMeadowlarkDocumentEmpty, MeadowlarkDocument } from '../../src/model/MeadowlarkDocument'; const newUpsertRequest = (): UpsertRequest => ({ meadowlarkId: '' as MeadowlarkId, @@ -77,9 +75,9 @@ describe('given the upsert of a new document', () => { }); it('should exist in the db', async () => { - const result = await client.query(findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql(meadowlarkId)); + const result: MeadowlarkId[] = await findAliasMeadowlarkIdsForDocumentByMeadowlarkId(client, meadowlarkId); - expect(result.rowCount).toBe(1); + expect(result.length).toBe(1); }); it('should return insert success', async () => { @@ -166,9 +164,9 @@ describe('given an upsert of an existing document that changes the edfiDoc', () }); it('should have the change in the db', async () => { - const result: any = await client.query(findDocumentByMeadowlarkIdSql(meadowlarkId)); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, meadowlarkId); - expect(result.rows[0].edfi_doc.call).toBe('two'); + expect(result.edfi_doc.call).toBe('two'); }); }); @@ -225,8 +223,8 @@ describe('given an upsert of a new document that references a non-existent docum }); it('should have inserted the document with an invalid reference in the db', async () => { - const result: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); - expect(result.rows[0].document_identity.natural).toBe('upsert4'); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesMeadowlarkId); + expect(result.document_identity.natural).toBe('upsert4'); }); }); @@ -306,8 +304,8 @@ describe('given an upsert of a new document that references an existing document }); it('should have inserted the document with a valid reference in the db', async () => { - const result: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); - expect(result.rows[0].document_identity.natural).toBe('upsert6'); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesMeadowlarkId); + expect(result.document_identity.natural).toBe('upsert6'); }); }); @@ -409,8 +407,8 @@ describe('given an upsert of a new document with one existing and one non-existe }); it('should not have inserted the document with an invalid reference in the db', async () => { - const result: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); - expect(result.rowCount).toEqual(0); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesMeadowlarkId); + expect(isMeadowlarkDocumentEmpty(result)).toEqual(true); }); }); @@ -496,8 +494,8 @@ describe('given an upsert of a subclass document when a different subclass has t }); it('should not have inserted the document with the same superclass identity in the db', async () => { - const result: any = await client.query(findDocumentByMeadowlarkIdSql(sameSuperclassIdentityMeadowlarkId)); - expect(result.rowCount).toEqual(0); + const result: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, sameSuperclassIdentityMeadowlarkId); + expect(isMeadowlarkDocumentEmpty(result)).toEqual(true); }); }); @@ -568,11 +566,11 @@ describe('given an update of a document that references a non-existent document }); it('should have updated the document with an invalid reference in the db', async () => { - const docResult: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); + const docResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesMeadowlarkId); const refsResult: any = await client.query(retrieveReferencesByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); const outboundRefs = refsResult.rows.map((ref) => ref.referenced_meadowlark_id); - expect(docResult.rows[0].document_identity.natural).toBe('upsert4'); + expect(docResult.document_identity.natural).toBe('upsert4'); expect(outboundRefs).toMatchInlineSnapshot(` [ "QtykK4uDYZK7VOChNxRsMDtOcAu6a0oe9ozl2Q", @@ -669,12 +667,12 @@ describe('given an update of a document that references an existing document wit }); it('should have updated the document with a valid reference in the db', async () => { - const docResult: any = await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); + const docResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(client, documentWithReferencesMeadowlarkId); const refsResult: any = await client.query(retrieveReferencesByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); const outboundRefs = refsResult.rows.map((ref) => ref.referenced_meadowlark_id); - expect(docResult.rows[0].document_identity.natural).toBe('upsert6'); + expect(docResult.document_identity.natural).toBe('upsert6'); expect(outboundRefs).toMatchInlineSnapshot(` [ "Qw5FvPdKxAXWnGghUWv5LKuA2cXaJPWJGJRDBQ", @@ -793,7 +791,7 @@ describe('given an update of a document with one existing and one non-existent r }); it('should not have updated the document with an invalid reference in the db', async () => { - await client.query(findDocumentByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); + await findDocumentByMeadowlarkId(client, documentWithReferencesMeadowlarkId); const refsResult: any = await client.query(retrieveReferencesByMeadowlarkIdSql(documentWithReferencesMeadowlarkId)); const outboundRefs = refsResult.rows.map((ref) => ref.referenced_meadowlark_id); diff --git a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/locking/Delete.test.ts b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/locking/Delete.test.ts index 29d838e5..bb1753ae 100644 --- a/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/locking/Delete.test.ts +++ b/Meadowlark-js/backends/meadowlark-postgresql-backend/test/integration/locking/Delete.test.ts @@ -23,17 +23,18 @@ import { PoolClient } from 'pg'; import { getSharedClient, resetSharedClient } from '../../../src/repository/Db'; import { validateReferences } from '../../../src/repository/ReferenceValidation'; import { - findReferencingMeadowlarkIdsSql, - deleteAliasesForDocumentByMeadowlarkIdSql, - findDocumentByMeadowlarkIdSql, - documentInsertOrUpdateSql, - findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql, - insertAliasSql, - insertOutboundReferencesSql, - deleteDocumentByDocumentUuIdSql, + findReferencingMeadowlarkIds, + deleteAliasesForDocumentByMeadowlarkId, + findDocumentByMeadowlarkId, + insertOrUpdateDocument, + findAliasMeadowlarkIdsForDocumentByMeadowlarkId, + insertAlias, + insertOutboundReferences, + deleteDocumentRowByDocumentUuid, } from '../../../src/repository/SqlHelper'; import { upsertDocument } from '../../../src/repository/Upsert'; import { deleteAll } from '../TestHelper'; +import { MeadowlarkDocument, isMeadowlarkDocumentEmpty } from '../../../src/model/MeadowlarkDocument'; // A bunch of setup stuff const newUpsertRequest = (): UpsertRequest => ({ @@ -133,16 +134,19 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted // Get the alias ids for the document we're trying to delete, because the update transaction is trying // to use our school, this is where the code will throw a locking error. This is technically the last line // of code in this try block that should execute - const aliasIdResult = await deleteClient.query(findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql(schoolMeadowlarkId)); + const aliasIdResult: MeadowlarkId[] = await findAliasMeadowlarkIdsForDocumentByMeadowlarkId( + deleteClient, + schoolMeadowlarkId, + ); - const validDocMeadowlarkIds = aliasIdResult.rows.map((ref) => ref.alias_meadowlark_id); - const referenceResult = await deleteClient.query(findReferencingMeadowlarkIdsSql(validDocMeadowlarkIds)); - const anyReferences = referenceResult.rows.filter((ref) => ref.meadowlark_id !== schoolMeadowlarkId); + const validDocMeadowlarkIds = aliasIdResult.map((ref) => ref); + const referenceResult: MeadowlarkId[] = await findReferencingMeadowlarkIds(deleteClient, validDocMeadowlarkIds); + const anyReferences: MeadowlarkId[] = referenceResult.filter((ref) => ref !== schoolMeadowlarkId); expect(anyReferences.length).toEqual(0); - await deleteClient.query(deleteDocumentByDocumentUuIdSql(resultDocumentUuid)); - await deleteClient.query(deleteAliasesForDocumentByMeadowlarkIdSql(schoolMeadowlarkId)); + await deleteDocumentRowByDocumentUuid(deleteClient, resultDocumentUuid); + await deleteAliasesForDocumentByMeadowlarkId(deleteClient, schoolMeadowlarkId); await deleteClient.query('COMMIT'); } catch (e1) { await deleteClient.query('ROLLBACK'); @@ -150,7 +154,8 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted } const documentUuid: DocumentUuid = '9ad5c9fa-82d1-494c-8d54-6aa1457f4364' as DocumentUuid; // Perform the insert of AcademicWeek document, adding a reference to to to-be-deleted document - const documentUpsertSql = documentInsertOrUpdateSql( + const insertResult = await insertOrUpdateDocument( + insertClient, { meadowlarkId: academicWeekMeadowlarkId, documentUuid, @@ -162,16 +167,14 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted }, true, ); - - const insertResult = await insertClient.query(documentUpsertSql); // eslint-disable-next-line no-restricted-syntax for (const ref of outboundRefs) { - await insertClient.query(insertOutboundReferencesSql(academicWeekMeadowlarkId, ref as MeadowlarkId)); - await insertClient.query(insertAliasSql(documentUuid, academicWeekMeadowlarkId, ref as MeadowlarkId)); + await insertOutboundReferences(insertClient, academicWeekMeadowlarkId, ref as MeadowlarkId); + await insertAlias(insertClient, documentUuid, academicWeekMeadowlarkId, ref as MeadowlarkId); } // **** The insert of AcademicWeek document should have been successful - expect(insertResult.rowCount).toEqual(1); + expect(insertResult).toEqual(true); // ---- // End transaction to insert the AcademicWeek document @@ -188,8 +191,8 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted }); it('should have still have the School document in the db - a success', async () => { - const docResult: any = await insertClient.query(findDocumentByMeadowlarkIdSql(schoolMeadowlarkId)); - expect(docResult.rows[0].document_identity.schoolId).toBe('123'); + const docResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(insertClient, schoolMeadowlarkId); + expect(docResult.document_identity.schoolId).toBe('123'); }); }); @@ -223,21 +226,24 @@ describe('given an insert concurrent with a delete referencing the to-be-deleted // Retrieve the alias ids for the school that we're trying to delete, this call will also // lock the school record so when we try to lock the records during the insert of the academic week // below it will fail - const aliasIdResult = await deleteClient.query(findAliasMeadowlarkIdsForDocumentByMeadowlarkIdSql(schoolMeadowlarkId)); + const aliasIdResult: MeadowlarkId[] = await findAliasMeadowlarkIdsForDocumentByMeadowlarkId( + deleteClient, + schoolMeadowlarkId, + ); // The school is in the database - expect(aliasIdResult.rowCount).toEqual(1); + expect(aliasIdResult.length).toEqual(1); // See if there are existing references to this document - const validDocMeadowlarkIds = aliasIdResult.rows.map((ref) => ref.alias_meadowlark_id); - const referenceResult = await deleteClient.query(findReferencingMeadowlarkIdsSql(validDocMeadowlarkIds)); - const anyReferences = referenceResult.rows.filter((ref) => ref.meadowlark_id !== schoolMeadowlarkId); + const validDocMeadowlarkIds = aliasIdResult.map((ref) => ref); + const referenceResult: MeadowlarkId[] = await findReferencingMeadowlarkIds(deleteClient, validDocMeadowlarkIds); + const anyReferences: MeadowlarkId[] = referenceResult.filter((ref) => ref !== schoolMeadowlarkId); expect(anyReferences.length).toEqual(0); // Delete the document - await deleteClient.query(deleteDocumentByDocumentUuIdSql(resultDocumentUuid)); - await deleteClient.query(deleteAliasesForDocumentByMeadowlarkIdSql(schoolMeadowlarkId)); + await deleteDocumentRowByDocumentUuid(deleteClient, resultDocumentUuid); + await deleteAliasesForDocumentByMeadowlarkId(deleteClient, schoolMeadowlarkId); // Start the insert await insertClient.query('BEGIN'); @@ -261,7 +267,8 @@ describe('given an insert concurrent with a delete referencing the to-be-deleted // Should be no reference validation failures for AcademicWeek document expect(upsertFailures).toHaveLength(0); const documentUuid: DocumentUuid = '9ad5c9fa-82d1-494c-8d54-6aa1457f4365' as DocumentUuid; - const documentUpsertSql = documentInsertOrUpdateSql( + const insertResult = await insertOrUpdateDocument( + insertClient, { meadowlarkId: academicWeekMeadowlarkId, documentUuid, @@ -273,13 +280,11 @@ describe('given an insert concurrent with a delete referencing the to-be-deleted }, true, ); - - const insertResult = await insertClient.query(documentUpsertSql); - expect(insertResult.rowCount).toEqual(0); + expect(insertResult).toEqual(true); // eslint-disable-next-line no-restricted-syntax for (const ref of outboundRefs) { - await insertClient.query(insertOutboundReferencesSql(academicWeekMeadowlarkId, ref as MeadowlarkId)); - await insertClient.query(insertAliasSql(documentUuid, academicWeekMeadowlarkId, ref as MeadowlarkId)); + await insertOutboundReferences(insertClient, academicWeekMeadowlarkId, ref as MeadowlarkId); + await insertAlias(insertClient, documentUuid, academicWeekMeadowlarkId, ref as MeadowlarkId); } await insertClient.query('COMMIT'); } catch (e1) { @@ -299,9 +304,9 @@ describe('given an insert concurrent with a delete referencing the to-be-deleted }); it('should have still have the School document in the db - a success', async () => { - const schoolDocResult: any = await insertClient.query(findDocumentByMeadowlarkIdSql(schoolMeadowlarkId)); - const awDocResult: any = await insertClient.query(findDocumentByMeadowlarkIdSql(academicWeekMeadowlarkId)); - expect(schoolDocResult.rowCount).toEqual(0); - expect(awDocResult.rowCount).toEqual(0); + const schoolDocResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(insertClient, schoolMeadowlarkId); + const awDocResult: MeadowlarkDocument = await findDocumentByMeadowlarkId(insertClient, academicWeekMeadowlarkId); + expect(isMeadowlarkDocumentEmpty(schoolDocResult)).toEqual(true); + expect(isMeadowlarkDocumentEmpty(awDocResult)).toEqual(true); }); }); diff --git a/Meadowlark-js/packages/meadowlark-core/src/handler/Delete.ts b/Meadowlark-js/packages/meadowlark-core/src/handler/Delete.ts index eb7d14f4..d3b53791 100644 --- a/Meadowlark-js/packages/meadowlark-core/src/handler/Delete.ts +++ b/Meadowlark-js/packages/meadowlark-core/src/handler/Delete.ts @@ -57,7 +57,7 @@ export async function deleteIt(frontendRequest: FrontendRequest): Promise { + if (referringDocumentInfo) { + referringDocumentInfo.forEach((document) => { let uri = resourceUriFrom( { version: versionAbbreviationFor(document.resourceVersion), diff --git a/Meadowlark-js/packages/meadowlark-core/src/index.ts b/Meadowlark-js/packages/meadowlark-core/src/index.ts index cd88539b..c5750a66 100644 --- a/Meadowlark-js/packages/meadowlark-core/src/index.ts +++ b/Meadowlark-js/packages/meadowlark-core/src/index.ts @@ -20,7 +20,7 @@ export type { DeleteRequest } from './message/DeleteRequest'; export type { UpdateRequest } from './message/UpdateRequest'; export type { UpsertRequest } from './message/UpsertRequest'; export type { QueryRequest } from './message/QueryRequest'; -export type { BlockingDocument } from './message/BlockingDocument'; +export type { ReferringDocumentInfo } from './message/ReferringDocumentInfo'; export type { PaginationParameters } from './message/PaginationParameters'; export type { Security } from './security/Security'; export { newSecurity } from './security/Security'; @@ -32,7 +32,7 @@ export type { FrontendRequest, Headers } from './handler/FrontendRequest'; export { newFrontendRequest, newFrontendRequestMiddleware } from './handler/FrontendRequest'; export type { FrontendResponse } from './handler/FrontendResponse'; export { newFrontendResponse, newFrontendResponseSuccess } from './handler/FrontendResponse'; -export { meadowlarkIdForDocumentIdentity, generateDocumentUuid } from './model/DocumentIdentity'; +export { meadowlarkIdForDocumentIdentity, generateDocumentUuid, NoDocumentIdentity } from './model/DocumentIdentity'; export { getMeadowlarkIdForDocumentReference } from './model/DocumentReference'; export type { DocumentInfo } from './model/DocumentInfo'; export { newDocumentInfo, NoDocumentInfo } from './model/DocumentInfo'; diff --git a/Meadowlark-js/packages/meadowlark-core/src/message/DeleteResult.ts b/Meadowlark-js/packages/meadowlark-core/src/message/DeleteResult.ts index 4e403b5c..4718c8e8 100644 --- a/Meadowlark-js/packages/meadowlark-core/src/message/DeleteResult.ts +++ b/Meadowlark-js/packages/meadowlark-core/src/message/DeleteResult.ts @@ -3,11 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import { BlockingDocument } from './BlockingDocument'; +import { ReferringDocumentInfo } from './ReferringDocumentInfo'; export type DeleteFailureReference = { response: 'DELETE_FAILURE_REFERENCE'; - blockingDocuments: BlockingDocument[]; + referringDocumentInfo: ReferringDocumentInfo[]; }; export type DeleteResult = diff --git a/Meadowlark-js/packages/meadowlark-core/src/message/BlockingDocument.ts b/Meadowlark-js/packages/meadowlark-core/src/message/ReferringDocumentInfo.ts similarity index 77% rename from Meadowlark-js/packages/meadowlark-core/src/message/BlockingDocument.ts rename to Meadowlark-js/packages/meadowlark-core/src/message/ReferringDocumentInfo.ts index 3be17071..001462d2 100644 --- a/Meadowlark-js/packages/meadowlark-core/src/message/BlockingDocument.ts +++ b/Meadowlark-js/packages/meadowlark-core/src/message/ReferringDocumentInfo.ts @@ -6,9 +6,9 @@ import { DocumentUuid, MeadowlarkId } from '../model/BrandedTypes'; /** - * Information on a document that is blocking the delete of another document for referential integrity reasons + * Information on a document that is referring another document for referential integrity reasons */ -export type BlockingDocument = { +export type ReferringDocumentInfo = { projectName: string; resourceVersion: string; resourceName: string; diff --git a/Meadowlark-js/packages/meadowlark-core/src/message/UpdateResult.ts b/Meadowlark-js/packages/meadowlark-core/src/message/UpdateResult.ts index 798103e2..a7873b63 100644 --- a/Meadowlark-js/packages/meadowlark-core/src/message/UpdateResult.ts +++ b/Meadowlark-js/packages/meadowlark-core/src/message/UpdateResult.ts @@ -3,12 +3,12 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import { BlockingDocument } from './BlockingDocument'; +import { ReferringDocumentInfo } from './ReferringDocumentInfo'; type UpdateFailureBlocked = { response: 'UPDATE_FAILURE_REFERENCE' | 'UPDATE_FAILURE_CONFLICT'; failureMessage?: string | object; - blockingDocuments: BlockingDocument[]; + referringDocumentInfo: ReferringDocumentInfo[]; }; export type UpdateResult = diff --git a/Meadowlark-js/packages/meadowlark-core/src/message/UpsertResult.ts b/Meadowlark-js/packages/meadowlark-core/src/message/UpsertResult.ts index 7f1b2b82..f809f1bb 100644 --- a/Meadowlark-js/packages/meadowlark-core/src/message/UpsertResult.ts +++ b/Meadowlark-js/packages/meadowlark-core/src/message/UpsertResult.ts @@ -4,12 +4,12 @@ // See the LICENSE and NOTICES files in the project root for more information. import { DocumentUuid } from '../model/BrandedTypes'; -import { BlockingDocument } from './BlockingDocument'; +import { ReferringDocumentInfo } from './ReferringDocumentInfo'; type UpsertFailureBlocked = { response: 'INSERT_FAILURE_REFERENCE' | 'INSERT_FAILURE_CONFLICT' | 'UPDATE_FAILURE_REFERENCE'; failureMessage?: string | object; - blockingDocuments: BlockingDocument[]; + referringDocumentInfo: ReferringDocumentInfo[]; }; export type UpsertResult = diff --git a/Meadowlark-js/packages/meadowlark-core/test/handler/DeleteIt.test.ts b/Meadowlark-js/packages/meadowlark-core/test/handler/DeleteIt.test.ts index a420af1d..19717f3d 100644 --- a/Meadowlark-js/packages/meadowlark-core/test/handler/DeleteIt.test.ts +++ b/Meadowlark-js/packages/meadowlark-core/test/handler/DeleteIt.test.ts @@ -8,7 +8,7 @@ import * as PluginLoader from '../../src/plugin/PluginLoader'; import { FrontendResponse } from '../../src/handler/FrontendResponse'; import { FrontendRequest, newFrontendRequest, newFrontendRequestMiddleware } from '../../src/handler/FrontendRequest'; import { NoDocumentStorePlugin } from '../../src/plugin/backend/NoDocumentStorePlugin'; -import { BlockingDocument } from '../../src/message/BlockingDocument'; +import { ReferringDocumentInfo } from '../../src/message/ReferringDocumentInfo'; import { DocumentUuid, MeadowlarkId } from '../../src/model/BrandedTypes'; const frontendRequest: FrontendRequest = { @@ -151,7 +151,7 @@ describe('given id does not exist', () => { describe('given the document to be deleted is referenced by other documents ', () => { let mockDocumentStore: any; - const expectedBlockingDocument: BlockingDocument = { + const expectedBlockingDocument: ReferringDocumentInfo = { resourceName: 'resourceName', documentUuid: 'documentUuid' as DocumentUuid, meadowlarkId: 'meadowlarkId' as MeadowlarkId, @@ -166,7 +166,7 @@ describe('given the document to be deleted is referenced by other documents ', ( deleteDocumentById: async () => Promise.resolve({ response: 'DELETE_FAILURE_REFERENCE', - blockingDocuments: [expectedBlockingDocument], + referringDocumentInfo: [expectedBlockingDocument], }), }); diff --git a/Meadowlark-js/packages/meadowlark-core/test/handler/Update.test.ts b/Meadowlark-js/packages/meadowlark-core/test/handler/Update.test.ts index 46072ad7..c42e0d68 100644 --- a/Meadowlark-js/packages/meadowlark-core/test/handler/Update.test.ts +++ b/Meadowlark-js/packages/meadowlark-core/test/handler/Update.test.ts @@ -8,7 +8,7 @@ import * as PluginLoader from '../../src/plugin/PluginLoader'; import { FrontendResponse } from '../../src/handler/FrontendResponse'; import { FrontendRequest, newFrontendRequest, newFrontendRequestMiddleware } from '../../src/handler/FrontendRequest'; import { NoDocumentStorePlugin } from '../../src/plugin/backend/NoDocumentStorePlugin'; -import { BlockingDocument } from '../../src/message/BlockingDocument'; +import { ReferringDocumentInfo } from '../../src/message/ReferringDocumentInfo'; import { DocumentUuid, MeadowlarkId } from '../../src/model/BrandedTypes'; const documentUuid = '2edb604f-eab0-412c-a242-508d6529214d' as DocumentUuid; @@ -61,7 +61,7 @@ describe('given the requested document does not exist', () => { describe('given the new document has an invalid reference ', () => { let mockDocumentStore: any; - const expectedBlockingDocument: BlockingDocument = { + const expectedBlockingDocument: ReferringDocumentInfo = { resourceName: 'resourceName', documentUuid: 'documentUuid' as DocumentUuid, meadowlarkId: 'meadowlarkId' as MeadowlarkId, @@ -78,7 +78,7 @@ describe('given the new document has an invalid reference ', () => { Promise.resolve({ response: 'UPDATE_FAILURE_REFERENCE', failureMessage: expectedError, - blockingDocuments: [expectedBlockingDocument], + referringDocumentInfo: [expectedBlockingDocument], }), }); diff --git a/Meadowlark-js/packages/meadowlark-core/test/handler/Upsert.test.ts b/Meadowlark-js/packages/meadowlark-core/test/handler/Upsert.test.ts index 1a844f1f..071fb8db 100644 --- a/Meadowlark-js/packages/meadowlark-core/test/handler/Upsert.test.ts +++ b/Meadowlark-js/packages/meadowlark-core/test/handler/Upsert.test.ts @@ -9,7 +9,7 @@ import * as PluginLoader from '../../src/plugin/PluginLoader'; import { FrontendRequest, newFrontendRequest, newFrontendRequestMiddleware } from '../../src/handler/FrontendRequest'; import { FrontendResponse } from '../../src/handler/FrontendResponse'; import { NoDocumentStorePlugin } from '../../src/plugin/backend/NoDocumentStorePlugin'; -import { BlockingDocument } from '../../src/message/BlockingDocument'; +import { ReferringDocumentInfo } from '../../src/message/ReferringDocumentInfo'; import { isDocumentUuidWellFormed } from '../../src/validation/DocumentIdValidator'; import { DocumentUuid, MeadowlarkId } from '../../src/model/BrandedTypes'; @@ -29,7 +29,7 @@ const frontendRequest: FrontendRequest = { describe('given persistence is going to throw a reference error on insert', () => { let response: FrontendResponse; - const expectedBlockingDocument: BlockingDocument = { + const expectedBlockingDocument: ReferringDocumentInfo = { resourceName: 'resourceName', documentUuid, meadowlarkId: 'meadowlarkId' as MeadowlarkId, @@ -46,7 +46,7 @@ describe('given persistence is going to throw a reference error on insert', () = Promise.resolve({ response: 'INSERT_FAILURE_REFERENCE', failureMessage: expectedError, - blockingDocuments: [expectedBlockingDocument], + referringDocumentInfo: [expectedBlockingDocument], }), }); @@ -113,7 +113,7 @@ describe('given upsert has write conflict failure', () => { describe('given persistence is going to throw a conflict error on insert', () => { let response: FrontendResponse; - const expectedBlockingDocument: BlockingDocument = { + const expectedBlockingDocument: ReferringDocumentInfo = { resourceName: 'resourceName', documentUuid, meadowlarkId: 'meadowlarkId' as MeadowlarkId, @@ -130,7 +130,7 @@ describe('given persistence is going to throw a conflict error on insert', () => Promise.resolve({ response: 'INSERT_FAILURE_CONFLICT', failureMessage: expectedError, - blockingDocuments: [expectedBlockingDocument], + referringDocumentInfo: [expectedBlockingDocument], }), }); @@ -161,7 +161,7 @@ describe('given persistence is going to throw a conflict error on insert', () => describe('given persistence is going to throw a reference error on update though did not on insert attempt', () => { let response: FrontendResponse; - const expectedBlockingDocument: BlockingDocument = { + const expectedBlockingDocument: ReferringDocumentInfo = { resourceName: 'resourceName', documentUuid, meadowlarkId: 'meadowlarkId' as MeadowlarkId, @@ -177,7 +177,7 @@ describe('given persistence is going to throw a reference error on update though Promise.resolve({ response: 'UPDATE_FAILURE_REFERENCE', failureMessage: 'Reference failure', - blockingDocuments: [expectedBlockingDocument], + referringDocumentInfo: [expectedBlockingDocument], }), });