From 285d982e91946487f42d5f75b28b6e30e4c1b1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Veillard?= Date: Wed, 19 Jun 2024 21:31:29 +0200 Subject: [PATCH] draft:intermediary-relations --- package.json | 2 +- src/helpers.ts | 19 +++ .../enrichSteps/addIntermediaryRelations.ts | 115 ++++++++++++++++++ src/stateMachine/mutation/bql/intermediary.ts | 30 +++-- src/stateMachine/mutation/bql/parse.ts | 20 ++- src/stateMachine/mutation/mutationMachine.ts | 28 ++++- src/stateMachine/mutation/tql/run.ts | 1 + src/stateMachine/query/bql/enrich.ts | 2 +- tests/unit/mutations/basic.ts | 106 +++++++++------- 9 files changed, 263 insertions(+), 60 deletions(-) create mode 100644 src/stateMachine/mutation/bql/enrichSteps/addIntermediaryRelations.ts diff --git a/package.json b/package.json index ba04ba4f..0ccba7dd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/helpers.ts b/src/helpers.ts index 7ba5ea7f..c6c2ff26 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -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) || []; diff --git a/src/stateMachine/mutation/bql/enrichSteps/addIntermediaryRelations.ts b/src/stateMachine/mutation/bql/enrichSteps/addIntermediaryRelations.ts new file mode 100644 index 00000000..ebd9eafb --- /dev/null +++ b/src/stateMachine/mutation/bql/enrichSteps/addIntermediaryRelations.ts @@ -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)})`, + ); + + } */ +}; diff --git a/src/stateMachine/mutation/bql/intermediary.ts b/src/stateMachine/mutation/bql/intermediary.ts index aca38a76..ab84db3b 100644 --- a/src/stateMachine/mutation/bql/intermediary.ts +++ b/src/stateMachine/mutation/bql/intermediary.ts @@ -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; }; diff --git a/src/stateMachine/mutation/bql/parse.ts b/src/stateMachine/mutation/bql/parse.ts index f2dcf93c..094db0c1 100644 --- a/src/stateMachine/mutation/bql/parse.ts +++ b/src/stateMachine/mutation/bql/parse.ts @@ -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, ) => { @@ -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, @@ -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) @@ -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. @@ -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}`, diff --git a/src/stateMachine/mutation/mutationMachine.ts b/src/stateMachine/mutation/mutationMachine.ts index 48219615..425592aa 100644 --- a/src/stateMachine/mutation/mutationMachine.ts +++ b/src/stateMachine/mutation/mutationMachine.ts @@ -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 = { @@ -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) => { @@ -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( @@ -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, ), diff --git a/src/stateMachine/mutation/tql/run.ts b/src/stateMachine/mutation/tql/run.ts index 4275822f..4ac6bf04 100644 --- a/src/stateMachine/mutation/tql/run.ts +++ b/src/stateMachine/mutation/tql/run.ts @@ -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); diff --git a/src/stateMachine/query/bql/enrich.ts b/src/stateMachine/query/bql/enrich.ts index 2f9e3775..9c699aa3 100644 --- a/src/stateMachine/query/bql/enrich.ts +++ b/src/stateMachine/query/bql/enrich.ts @@ -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 }; diff --git a/tests/unit/mutations/basic.ts b/tests/unit/mutations/basic.ts index cfe57385..e1f17c8a 100644 --- a/tests/unit/mutations/basic.ts +++ b/tests/unit/mutations/basic.ts @@ -2,7 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { deepSort, expectArraysInObjectToContainSameElements } from '../../helpers/matchers'; import { createTest } from '../../helpers/createTest'; -import { expect, it } from 'vitest'; +import { expect, expect, it } from 'vitest'; export const testBasicMutation = createTest('Mutation: Basic', (ctx) => { // some random issues forced a let here @@ -142,50 +142,61 @@ export const testBasicMutation = createTest('Mutation: Basic', (ctx) => { }, ], }; + const res = await ctx.mutate(user); - expect(res).toMatchObject([ - { - $thing: 'User', - $op: 'create', - id: user.id, - }, - { - $thing: 'Account', - $thingType: 'entity', - $op: 'create', - id: user.accounts[0].id, - profile: { - hobby: ['Running'], + console.log('res', res); + + try { + expect(deepSort(res, '$thing')).toMatchObject([ + { + $thing: 'Account', + $thingType: 'entity', + $op: 'create', + id: user.accounts[0].id, + profile: { + hobby: ['Running'], + }, }, - }, - { - $thing: 'User-Accounts', - $op: 'create', - accounts: user.accounts[0].id, - user: user.id, - }, - ]); - const deleteRes = await ctx.mutate({ - $thing: 'User', - $op: 'delete', - $id: user.id, - accounts: [{ $op: 'delete' }], - }); - expect(deleteRes).toMatchObject([ - { - $op: 'delete', + { + $thing: 'User', + $op: 'create', + id: user.id, + }, + { + $thing: 'User-Accounts', + $op: 'create', + }, + { + $thing: 'User-Accounts', + $op: 'link', + accounts: user.accounts[0].id, + user: user.id, + }, + ]); + } finally { + //to avoid impact on tests u1-u4 + const deleteRes = await ctx.mutate({ $thing: 'User', - $id: user.id, - }, - { - $op: 'delete', - $thing: 'Account', - }, - { $op: 'delete', - $thing: 'User-Accounts', - }, - ]); + $id: user.id, + accounts: [{ $op: 'delete' }], + }); + expect(deepSort(deleteRes, '$thing')).toMatchObject([ + { + $op: 'delete', + $thing: 'Account', + }, + { + $op: 'delete', + $thing: 'User', + $id: user.id, + }, + { + $op: 'delete', + $thing: 'User-Accounts', + }, + ]); + } }); it('b2a[update] Basic', async () => { @@ -428,7 +439,7 @@ export const testBasicMutation = createTest('Mutation: Basic', (ctx) => { }, ]); }); - it('b3rn[delete, relation, nested] Basic', async () => { + it.only('b3rn[delete, relation, nested] Basic', async () => { //create nested object await ctx.mutate( { @@ -454,6 +465,8 @@ export const testBasicMutation = createTest('Mutation: Basic', (ctx) => { }, { noMetadata: true }, ); + + console.log('res1', res1); expect(deepSort(res1, 'id')).toEqual({ 'user-tags': [ { id: 'ustag1', color: 'pink' }, @@ -476,6 +489,15 @@ export const testBasicMutation = createTest('Mutation: Basic', (ctx) => { }, // { preQuery: false }, ); + const res11 = await ctx.query( + { + $entity: 'User', + $id: 'u2', + $fields: [{ $path: 'user-tags', $fields: ['id', 'color'] }], + }, + { noMetadata: true }, + ); + console.log('res11', res11); const res2 = await ctx.query( {