Skip to content

Commit

Permalink
feat: add idGenerator to ExecutionOptions to override default behavior
Browse files Browse the repository at this point in the history
This can be useful to e.g. generate predictable IDs for testing
purposes.
  • Loading branch information
Yogu committed Nov 9, 2023
1 parent b01ce1e commit 35281e3
Show file tree
Hide file tree
Showing 10 changed files with 69 additions and 14 deletions.
2 changes: 2 additions & 0 deletions core-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export {
ExecutionOptions,
MutationMode,
ExecutionOptionsCallbackArgs,
Clock,
IDGenerator,
} from './src/execution/execution-options';
export { ExecutionResult } from './src/execution/execution-result';
export {
Expand Down
8 changes: 7 additions & 1 deletion src/database/arangodb/aql-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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 {
Expand Down Expand Up @@ -1903,6 +1908,7 @@ export function getAQLQuery(
undefined,
new QueryContext({
clock: options.clock ?? new DefaultClock(),
idGenerator: options.idGenerator ?? new UUIDGenerator(),
}),
);
}
Expand Down
1 change: 1 addition & 0 deletions src/database/arangodb/arangodb-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 11 additions & 7 deletions src/database/inmemory/inmemory-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
Expand Down Expand Up @@ -202,6 +201,8 @@ export class InMemoryAdapter implements DatabaseAdapter {
}
return [arg];
},

generateID: () => idGenerator.generateID({ target: 'root-entity' }),
};

let resultHolder: { [p: string]: any } = {};
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
};
Expand Down
10 changes: 8 additions & 2 deletions src/database/inmemory/js-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 {
Expand Down Expand Up @@ -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};`,
Expand Down Expand Up @@ -1276,6 +1281,7 @@ export function getJSQuery(
undefined,
new QueryContext({
clock: options.clock ?? new DefaultClock(),
idGenerator: options.idGenerator ?? new UUIDGenerator(),
}),
);
}
Expand Down
29 changes: 29 additions & 0 deletions src/execution/execution-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OperationDefinitionNode } from 'graphql';
import { AuthContext } from '../authorization/auth-basics';
import { randomUUID } from 'crypto';

export type MutationMode = 'normal' | 'disallowed' | 'rollback';

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}
}
3 changes: 2 additions & 1 deletion src/execution/operation-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/schema-generation/create-input-types/input-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
7 changes: 6 additions & 1 deletion src/schema-generation/query-node-object-type/context.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -33,6 +33,7 @@ export function createRootFieldContext(
selectionTokenStack: [],
selectionToken: new SelectionToken(),
clock: options.clock ?? new DefaultClock(),
idGenerator: options.idGenerator ?? new UUIDGenerator(),
...options,
};
}
Expand Down

0 comments on commit 35281e3

Please sign in to comment.