Skip to content

Commit

Permalink
draft:intermediary-relations
Browse files Browse the repository at this point in the history
  • Loading branch information
lveillard committed Jun 19, 2024
1 parent d1aaa30 commit 285d982
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 60 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"test:surrealdb-ignoreTodo": "./tests/test.sh surrealdb/unit/queries -t \"^(?!.*TODO{.*[S].*}:).*\" ",
"test:surrealdb-query": "./tests/test.sh surrealdb/unit/queries/query.test.ts",
"test:typedb-ignoreTodo": "vitest run typedb -t \"^(?!.*TODO{.*[T].*}:).*\" ",
"test:typedb-mutation": "vitest run typedb/unit/mutations",
"test:typedb-mutation": "vitest run typedb/unit/mutations/basic.test.ts",
"test:typedb-query": "vitest run typedb/unit/queries --watch",
"test:typedb-schema": "vitest run typedb/unit/schema",
"types": "tsc --noEmit",
Expand Down
19 changes: 19 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,25 @@ export const enrichSchema = (schema: BormSchema, dbHandles: DBHandles): Enriched
];
}
if (linkField.target === 'role') {
// This gows through the relation path:
const toRelationPath =
val.linkFields?.filter((lf) => lf.target === 'relation' && lf.relation === linkField.relation) || [];
if (toRelationPath.length === 0) {
linkField.throughtRelationPath = `${linkField.relation}`;
val.linkFields?.push({
path: `${linkField.relation}`,
target: 'relation',
relation: linkField.relation,
cardinality: linkField.cardinality,
plays: linkField.plays,
});
} else if (toRelationPath.length > 1) {
throw new Error(
`[Schema] linkFields of target role cant have multiple paths to the intermediary relation. Thing: "${val.name}" LinkField: "${linkField.path}. Path:${meta.nodePath}.`,
);
} else {
linkField.throughtRelationPath = toRelationPath[0].path;
}
///target role
const allOppositeLinkFields =
allLinkedFields.filter((x) => x.relation === linkField.relation && x.plays !== linkField.plays) || [];
Expand Down
115 changes: 115 additions & 0 deletions src/stateMachine/mutation/bql/enrichSteps/addIntermediaryRelations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* eslint-disable no-param-reassign */
import { isArray } from 'radash';
import type { BQLMutationBlock, EnrichedLinkField } from '../../../../types';
import { nanoid } from 'nanoid';

export const addIntermediaryRelations = (node: BQLMutationBlock, field: string, fieldSchema: EnrichedLinkField) => {
if (fieldSchema.isVirtual) {
throw new Error(`[Mutation Error] Virtual fields cannot be used in mutations. (Field: ${field})`);
}
const isArrayField = isArray(node[field]);
const subNodes = (isArrayField ? node[field] : [node[field]]) as BQLMutationBlock[];
//console.log('NODE:', JSON.stringify(node, null, 2), 'and', JSON.stringify(subNodes, null, 2));

const { relation, oppositeLinkFieldsPlayedBy } = fieldSchema;

if (oppositeLinkFieldsPlayedBy.length !== 1) {
throw new Error(`[Mutation Error] Only one oppositeLinkFieldsPlayedBy is supported. (Field: ${field})`);
}

const pathToRelation = fieldSchema.throughtRelationPath; //todo in the enrich schema move this to linkField.targetRoles
const ParentRole = fieldSchema.plays;
const TargetRole = fieldSchema.oppositeLinkFieldsPlayedBy[0].plays;
//case 1: parent create means is a link
if (node.$op === 'create') {
const intermediaryRelations = subNodes.map((subNode) => ({
$op: 'create',
$thing: relation,
$thingType: 'relation',
$bzId: `IR_${nanoid()}`,
[TargetRole]: subNode,
[ParentRole]: { $bzId: node.$bzId, $thing: node.$thing, $thingType: node.$thingType, $op: 'link' },
}));

if (isArrayField) {
//this in the future could depend on how the intermediary relation is configured in the roleField, but by default it will create one intermediary relation per edge
node[pathToRelation] = intermediaryRelations;
} else {
// eslint-disable-next-line prefer-destructuring
node[pathToRelation] = intermediaryRelations[0];
}
}
if (node.$op === 'update' || node.$op === 'match' || node.$op === 'delete') {
const getOp = (subNode: BQLMutationBlock) => {
if (!subNode.$op) {
throw new Error(`[Mutation Error] Update and match operations require a $op field. (Field: ${field})`);
}
if (['update', 'match'].includes(subNode.$op)) {
return 'match';
}
if (subNode.$op === 'link') {
return 'create';
}
if (subNode.$op === 'unlink') {
return 'delete';
}
if (subNode.$op === 'delete') {
return 'delete';
}
throw new Error(`[Mutation Error] Invalid $op field. (Field: ${field})`);
};
//CASOS
//`uodateando un children
const intermediaryRelations: any = subNodes.map((subNode) => ({
$op: getOp(subNode),
$thing: relation,
$thingType: 'relation',
$bzId: `IR_${nanoid()}`,
[TargetRole]: subNode,
[ParentRole]: { $bzId: node.$bzId, $thing: node.$thing, $thingType: node.$thingType, $op: 'match' },
}));

if (isArrayField) {
//this in the future could depend on how the intermediary relation is configured in the roleField, but by default it will create one intermediary relation per edge
node[pathToRelation] = intermediaryRelations;
} else {
// eslint-disable-next-line prefer-destructuring
node[pathToRelation] = intermediaryRelations[0];
}
//
}
// eslint-disable-next-line no-param-reassign

/* /// Only objects, is fine
if (subNodes.every((child: unknown) => typeof child === 'object')) {
return;
///all strings, then we proceed to replace
} else if (subNodes.every((child: unknown) => typeof child === 'string')) {
const oppositePlayers = getOppositePlayers(field, fieldSchema);
const [player] = oppositePlayers;
//if parent === create, then is a link
const $op = node.$op === 'create' ? 'link' : 'replace';
const $thing = player.thing;
const $thingType = player.thingType;
//todo _: tempId included in the array, or as a single one of them
if (subNodes.some((child: unknown) => (child as string).startsWith('_:'))) {
throw new Error('[Not supported] At least one child of a replace is a tempId');
}
// eslint-disable-next-line no-param-reassign
node[field] = {
$id: node[field],
$op,
$thing,
$thingType,
$bzId: `S_${uuidv4()}`,
};
} else {
throw new Error(
`[Mutation Error] Replace can only be used with a single id or an array of ids. (Field: ${field} Nodes: ${JSON.stringify(subNodes)} Parent: ${JSON.stringify(node, null, 2)})`,
);
} */
};
30 changes: 23 additions & 7 deletions src/stateMachine/mutation/bql/intermediary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,35 @@ import { produce } from 'immer';
import type { TraversalCallbackContext } from 'object-traversal';
import { traverse } from 'object-traversal';
import type { EnrichedBQLMutationBlock, EnrichedBormSchema } from '../../../types';
import { isObject } from 'radash';
import { getFieldSchema } from '../../../helpers';
import { addIntermediaryRelations } from './enrichSteps/addIntermediaryRelations';

export const addIntermediaryRelationsBQLMutation = (
blocks: EnrichedBQLMutationBlock | EnrichedBQLMutationBlock[],
schema: EnrichedBormSchema,
) => {
const rootBlock = { $root: { $subRoot: blocks } };
const result = produce(rootBlock, (draft) =>
traverse(draft, ({ value: val, parent, key }: TraversalCallbackContext) => {
if (parent || val || key || schema) {
return;
const result = produce(blocks, (draft) =>
traverse(draft, ({ value }: TraversalCallbackContext) => {
if (isObject(value)) {
const node = value as EnrichedBQLMutationBlock;

Object.keys(node).forEach((field) => {
if (!field || field.startsWith('$')) {
return;
}
const fieldSchema = getFieldSchema(schema, node, field);
if (!fieldSchema) {
throw new Error(`[Internal] Field ${field} not found in schema`);
}

if (fieldSchema.fieldType === 'linkField' && fieldSchema.target === 'role') {
addIntermediaryRelations(node, field, fieldSchema);
return delete node[field]; //we return because we dont need to keep doing things in node[field]
}
});
}
}),
);

return result.$root.$subRoot;
return result;
};
20 changes: 15 additions & 5 deletions src/stateMachine/mutation/bql/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { computeField } from '../../../engine/compute';
import { deepRemoveMetaData } from '../../../../tests/helpers/matchers';
import { EdgeSchema, EdgeType } from '../../../types/symbols';

export const parseBQLMutation = async (
export const parseBQLMutation = (
blocks: EnrichedBQLMutationBlock | EnrichedBQLMutationBlock[],
schema: EnrichedBormSchema,
) => {
Expand Down Expand Up @@ -118,7 +118,6 @@ export const parseBQLMutation = async (
throw new Error('[internal error] BzId not found');
}
/// this is used to group the right delete/unlink operations with the involved things

const currentThingSchema = getCurrentSchema(schema, value);
const {
dataFields: dataFieldPaths,
Expand Down Expand Up @@ -248,8 +247,10 @@ export const parseBQLMutation = async (
// const testVal = {};

// todo: stuff 😂
//@ts-expect-error - TODO
toEdges(edgeType1);
if (ownRelation) {
//@ts-expect-error - TODO
toEdges(edgeType1);
}

/// when it has a parent through a linkField, we need to add an additional node (its dependency), as well as a match
/// no need for links, as links will have all the related things in the "link" object. While unlinks required dependencies as match and deletions as unlink (or dependencies would be also added)
Expand Down Expand Up @@ -405,8 +406,9 @@ export const parseBQLMutation = async (

return [nodes, edges];
};

const [parsedThings, parsedEdges] = listNodes(blocks);
console.log('parsedThings', parsedThings);
console.log('parsedEdges', parsedEdges);

//console.log('parsedThings', parsedThings);
/// some cases where we extract things, they must be ignored.
Expand Down Expand Up @@ -438,6 +440,14 @@ export const parseBQLMutation = async (
if (acc[existingIndex].$op === 'update' && thing.$op === 'update') {
return [...acc.slice(0, existingIndex), { ...acc[existingIndex], ...thing }, ...acc.slice(existingIndex + 1)];
}
if (acc[existingIndex].$op === 'delete' && thing.$op === 'match') {
//merge them
return [
...acc.slice(0, existingIndex),
{ ...acc[existingIndex], ...thing, $op: 'delete' },
...acc.slice(existingIndex + 1),
];
}
// For all other cases, throw an error
throw new Error(
`[Wrong format] Wrong operation combination for $tempId/$id "${thing.$tempId || thing.$id}". Existing: ${acc[existingIndex].$op}. Current: ${thing.$op}`,
Expand Down
28 changes: 24 additions & 4 deletions src/stateMachine/mutation/mutationMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { createMachine, transition, reduce, guard, interpret, state, invoke } fr
import { stringify } from './bql/stringify';
import { preHookDependencies } from './bql/enrichSteps/preHookDependencies';
import { dependenciesGuard } from './bql/guards/dependenciesGuard';
import { addIntermediaryRelationsBQLMutation } from './bql/intermediary';

const final = state;
type MachineContext = {
Expand Down Expand Up @@ -96,21 +97,35 @@ const updateTQLRes = (ctx: MachineContext, event: any) => {
// ============================================================================

const enrich = async (ctx: MachineContext) => {
return Object.keys(ctx.bql.current).length
const result = Object.keys(ctx.bql.current).length
? enrichBQLMutation(ctx.bql.current, ctx.schema, ctx.config)
: enrichBQLMutation(ctx.bql.raw, ctx.schema, ctx.config);

console.log('enriched', JSON.stringify(result, null, 2));
return result;
};

const preQuery = async (ctx: MachineContext) => {
return mutationPreQuery(ctx.bql.current, ctx.schema, ctx.config, ctx.handles);
const result = mutationPreQuery(ctx.bql.current, ctx.schema, ctx.config, ctx.handles);
console.log('preQuery', await result);
return result;
};

const preQueryDependencies = async (ctx: MachineContext) => {
return preHookDependencies(ctx.bql.current, ctx.schema, ctx.config, ctx.handles);
};

const addIntermediaryRelations = async (ctx: MachineContext) => {
console.log('before addIntermediaryRelations', JSON.stringify(ctx.bql.current, null, 2));
const result = addIntermediaryRelationsBQLMutation(ctx.bql.current, ctx.schema);
console.log('after addIntermediaryRelations', JSON.stringify(result, null, 2));
return result;
};

const parseBQL = async (ctx: MachineContext) => {
return parseBQLMutation(ctx.bql.current, ctx.schema);
const result = parseBQLMutation(ctx.bql.current, ctx.schema);
console.log('result', result);
return result;
};

const buildMutation = async (ctx: MachineContext) => {
Expand Down Expand Up @@ -160,7 +175,7 @@ export const machine = createMachine(
enrich: invoke(
enrich,
transition('done', 'preQuery', guard(requiresPreQuery), reduce(updateBqlReq)),
transition('done', 'parseBQL', reduce(updateBqlReq)),
transition('done', 'addIntermediaryRelation', reduce(updateBqlReq)),
errorTransition,
),
preHookDependencies: invoke(
Expand All @@ -171,6 +186,11 @@ export const machine = createMachine(
preQuery: invoke(
preQuery,
transition('done', 'preHookDependencies', guard(requiresPreHookDependencies), reduce(updateBqlReq)),
transition('done', 'addIntermediaryRelations', reduce(updateBqlReq)),
errorTransition,
),
addIntermediaryRelations: invoke(
addIntermediaryRelations,
transition('done', 'parseBQL', reduce(updateBqlReq)),
errorTransition,
),
Expand Down
1 change: 1 addition & 0 deletions src/stateMachine/mutation/tql/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const runTQLMutation = async (tqlMutation: TqlMutation, dbHandles: DBHand
throw new Error('TQL request error, no things');
}

//console.log('tqlMutation', tqlMutation);
const { session } = await getSessionOrOpenNewOne(dbHandles, config);
const mutateTransaction = await session.transaction(TransactionType.WRITE);

Expand Down
2 changes: 1 addition & 1 deletion src/stateMachine/query/bql/enrich.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ const createLinkField = (props: {
}): EnrichedLinkQuery => {
const { field, fieldStr, linkField, $justId, dbPath, schema, fieldSchema } = props;
const { target, oppositeLinkFieldsPlayedBy } = linkField;
return oppositeLinkFieldsPlayedBy.map((playedBy: any) => {
return oppositeLinkFieldsPlayedBy?.map((playedBy: any) => {
const $thingType = target === 'role' ? playedBy.thingType : 'relation';
const $thing = target === 'role' ? playedBy.thing : linkField.relation;
const node = { [`$${$thingType}`]: $thing };
Expand Down
Loading

0 comments on commit 285d982

Please sign in to comment.