diff --git a/core-exports.ts b/core-exports.ts index e91512f0..7b4c05de 100644 --- a/core-exports.ts +++ b/core-exports.ts @@ -13,6 +13,8 @@ export { ExecutionOptions, MutationMode, ExecutionOptionsCallbackArgs, + Clock, + IDGenerator, } from './src/execution/execution-options'; export { ExecutionResult } from './src/execution/execution-result'; export { diff --git a/src/database/arangodb/aql-generator.ts b/src/database/arangodb/aql-generator.ts index 954d90ce..fd5b8f41 100644 --- a/src/database/arangodb/aql-generator.ts +++ b/src/database/arangodb/aql-generator.ts @@ -87,7 +87,7 @@ import { getCollectionNameForRootEntity, } from './arango-basics'; import { getFlexSearchViewNameForRootEntity } from './schema-migration/arango-search-helpers'; -import { Clock, DefaultClock } from '../../execution/execution-options'; +import { Clock, DefaultClock, IDGenerator, UUIDGenerator } from '../../execution/execution-options'; enum AccessType { /** @@ -112,6 +112,11 @@ export interface QueryGenerationOptions { * An interface to determine the current date/time */ readonly clock: Clock; + + /** + * An interface to generate IDs, e.g. for new child entities. + */ + readonly idGenerator: IDGenerator; } class QueryContext { @@ -1903,6 +1908,7 @@ export function getAQLQuery( undefined, new QueryContext({ clock: options.clock ?? new DefaultClock(), + idGenerator: options.idGenerator ?? new UUIDGenerator(), }), ); } diff --git a/src/database/arangodb/arangodb-adapter.ts b/src/database/arangodb/arangodb-adapter.ts index d719b11f..66d9a025 100644 --- a/src/database/arangodb/arangodb-adapter.ts +++ b/src/database/arangodb/arangodb-adapter.ts @@ -333,6 +333,7 @@ export class ArangoDBAdapter implements DatabaseAdapter { //TODO Execute single statement AQL queries directly without "db.transaction"? aqlQuery = getAQLQuery(queryTree, { clock: options.clock, + idGenerator: options.idGenerator, }); executableQueries = aqlQuery.getExecutableQueries(); } finally { diff --git a/src/database/inmemory/inmemory-adapter.ts b/src/database/inmemory/inmemory-adapter.ts index 67bf9b78..5063b59a 100644 --- a/src/database/inmemory/inmemory-adapter.ts +++ b/src/database/inmemory/inmemory-adapter.ts @@ -15,14 +15,10 @@ import { getCollectionNameForRelation, getCollectionNameForRootEntity } from './ import { JSCompoundQuery, JSExecutableQuery } from './js'; import { getJSQuery } from './js-generator'; import { v4 as uuid } from 'uuid'; -import { DefaultClock } from '../../execution/execution-options'; +import { DefaultClock, IDGenerator, UUIDGenerator } from '../../execution/execution-options'; export class InMemoryDB { collections: { [name: string]: any[] } = {}; - - generateID() { - return uuid(); - } } export class InMemoryAdapter implements DatabaseAdapter { @@ -45,7 +41,10 @@ export class InMemoryAdapter implements DatabaseAdapter { * Gets the javascript source code for a function that executes a transaction * @returns {string} */ - private executeQueries(queries: JSExecutableQuery[]) { + private executeQueries( + queries: JSExecutableQuery[], + { idGenerator }: { idGenerator: IDGenerator }, + ) { const validators = new Map( ALL_QUERY_RESULT_VALIDATOR_FUNCTION_PROVIDERS.map((provider): [string, Function] => [ provider.getValidatorName(), @@ -202,6 +201,8 @@ export class InMemoryAdapter implements DatabaseAdapter { } return [arg]; }, + + generateID: () => idGenerator.generateID({ target: 'root-entity' }), }; let resultHolder: { [p: string]: any } = {}; @@ -266,6 +267,7 @@ export class InMemoryAdapter implements DatabaseAdapter { try { jsQuery = getJSQuery(args.queryTree, { clock: args.clock ?? new DefaultClock(), + idGenerator: args.idGenerator ?? new UUIDGenerator(), }); executableQueries = jsQuery.getExecutableQueries(); } finally { @@ -275,7 +277,9 @@ export class InMemoryAdapter implements DatabaseAdapter { this.logger.trace(`Executing JavaScript: ${jsQuery.toColoredString()}`); } - const data = this.executeQueries(executableQueries); + const data = this.executeQueries(executableQueries, { + idGenerator: args.idGenerator ?? new UUIDGenerator(), + }); return { data, }; diff --git a/src/database/inmemory/js-generator.ts b/src/database/inmemory/js-generator.ts index 38a48bf2..ac7ab926 100644 --- a/src/database/inmemory/js-generator.ts +++ b/src/database/inmemory/js-generator.ts @@ -70,7 +70,7 @@ import { Constructor, decapitalize } from '../../utils/utils'; import { likePatternToRegExp } from '../like-helpers'; import { getCollectionNameForRelation, getCollectionNameForRootEntity } from './inmemory-basics'; import { js, JSCompoundQuery, JSFragment, JSQueryResultVariable, JSVariable } from './js'; -import { Clock, DefaultClock } from '../../execution/execution-options'; +import { Clock, DefaultClock, IDGenerator, UUIDGenerator } from '../../execution/execution-options'; const ID_FIELD_NAME = 'id'; @@ -79,6 +79,11 @@ export interface QueryGenerationOptions { * An interface to determine the current date/time */ readonly clock: Clock; + + /** + * An interface to generate IDs, e.g. for new child entities. + */ + readonly idGenerator: IDGenerator; } class QueryContext { @@ -870,7 +875,7 @@ register(CreateEntityQueryNode, (node, context) => { const idVar = js.variable('id'); return jsExt.executingFunction( js`const ${objVar} = ${processNode(node.objectNode, context)};`, - js`const ${idVar} = db.generateID();`, + js`const ${idVar} = support.generateID();`, js`${objVar}.${js.identifier(ID_FIELD_NAME)} = ${idVar};`, js`${js.collection(getCollectionNameForRootEntity(node.rootEntityType))}.push(${objVar});`, js`return ${idVar};`, @@ -1276,6 +1281,7 @@ export function getJSQuery( undefined, new QueryContext({ clock: options.clock ?? new DefaultClock(), + idGenerator: options.idGenerator ?? new UUIDGenerator(), }), ); } diff --git a/src/execution/execution-options.ts b/src/execution/execution-options.ts index 0761f99d..1594cae9 100644 --- a/src/execution/execution-options.ts +++ b/src/execution/execution-options.ts @@ -1,5 +1,6 @@ import { OperationDefinitionNode } from 'graphql'; import { AuthContext } from '../authorization/auth-basics'; +import { randomUUID } from 'crypto'; export type MutationMode = 'normal' | 'disallowed' | 'rollback'; @@ -90,6 +91,12 @@ export interface ExecutionOptions { * An interface to determine the current date/time. If not specified, system time is used */ readonly clock?: Clock; + + /** + * An interface to generate IDs, e.g. for new child entities. If not specified, random UUIDs + * will be used. + */ + readonly idGenerator?: IDGenerator; } export interface TimeToLiveExecutionOptions { @@ -146,3 +153,25 @@ export class DefaultClock implements Clock { return new Date().toISOString(); } } + +export type IDGenerationTarget = 'root-entity' | 'child-entity'; + +export interface IDGenerationInfo { + readonly target: IDGenerationTarget; +} + +export interface IDGenerator { + /** + * Generate an id that will be used for some entities (e.g. child entities) + */ + generateID(info: IDGenerationInfo): string; +} + +export class UUIDGenerator implements IDGenerator { + /** + * Generates a random UUID + */ + generateID(): string { + return randomUUID(); + } +} diff --git a/src/execution/operation-resolver.ts b/src/execution/operation-resolver.ts index 6d24e6e1..d8d9b722 100644 --- a/src/execution/operation-resolver.ts +++ b/src/execution/operation-resolver.ts @@ -32,7 +32,7 @@ import { } from '../schema-generation/query-node-object-type'; import { SchemaTransformationContext } from '../schema/preparation/transformation-pipeline'; import { getPreciseTime, Watch } from '../utils/watch'; -import { DefaultClock, ExecutionOptions } from './execution-options'; +import { DefaultClock, ExecutionOptions, UUIDGenerator } from './execution-options'; import { ExecutionResult } from './execution-result'; export class OperationResolver { @@ -101,6 +101,7 @@ export class OperationResolver { options.flexSearchMaxFilterableAndSortableAmount, flexSearchRecursionDepth: options.flexSearchRecursionDepth, clock: options.clock ?? new DefaultClock(), + idGenerator: options.idGenerator ?? new UUIDGenerator(), }; queryTree = buildConditionalObjectQueryNode( rootQueryNode, diff --git a/src/schema-generation/create-input-types/input-types.ts b/src/schema-generation/create-input-types/input-types.ts index 3f228c14..d180554f 100644 --- a/src/schema-generation/create-input-types/input-types.ts +++ b/src/schema-generation/create-input-types/input-types.ts @@ -228,7 +228,7 @@ export class CreateChildEntityInputType extends CreateObjectInputType { getAdditionalProperties(value: PlainObject, context: FieldContext) { const now = context.clock.getCurrentTimestamp(); return { - [ID_FIELD]: uuid(), + [ID_FIELD]: context.idGenerator.generateID({ target: 'child-entity' }), [ENTITY_CREATED_AT]: now, [ENTITY_UPDATED_AT]: now, }; diff --git a/src/schema-generation/query-node-object-type/context.ts b/src/schema-generation/query-node-object-type/context.ts index 3f0a0319..08864b67 100644 --- a/src/schema-generation/query-node-object-type/context.ts +++ b/src/schema-generation/query-node-object-type/context.ts @@ -1,5 +1,5 @@ import { FieldSelection } from '../../graphql/query-distiller'; -import { Clock } from '../../execution/execution-options'; +import { Clock, IDGenerator } from '../../execution/execution-options'; /** * A token that corresponds to a FieldSelection but is local to one execution @@ -37,4 +37,9 @@ export interface FieldContext { * An interface to determine the current date/time */ readonly clock: Clock; + + /** + * An interface to generate IDs, e.g. for new child entities + */ + readonly idGenerator: IDGenerator; } diff --git a/src/schema-generation/query-node-object-type/query-node-generator.ts b/src/schema-generation/query-node-object-type/query-node-generator.ts index 3563702e..b6b12880 100644 --- a/src/schema-generation/query-node-object-type/query-node-generator.ts +++ b/src/schema-generation/query-node-object-type/query-node-generator.ts @@ -21,7 +21,7 @@ import { decapitalize, flatMap } from '../../utils/utils'; import { FieldContext, SelectionToken } from './context'; import { QueryNodeField, QueryNodeObjectType } from './definition'; import { extractQueryTreeObjectType, isListTypeIgnoringNonNull } from './utils'; -import { DefaultClock } from '../../execution/execution-options'; +import { DefaultClock, UUIDGenerator } from '../../execution/execution-options'; export function createRootFieldContext( options: Partial< @@ -33,6 +33,7 @@ export function createRootFieldContext( selectionTokenStack: [], selectionToken: new SelectionToken(), clock: options.clock ?? new DefaultClock(), + idGenerator: options.idGenerator ?? new UUIDGenerator(), ...options, }; }