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 ba04ba4..0ccba7d 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 7ba5ea7..c6c2ff2 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 0000000..ebd9eaf --- /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 aca38a7..ab84db3 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 f2dcf93..094db0c 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 4821961..425592a 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 4275822..4ac6bf0 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 2f9e377..9c699aa 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 cfe5738..e1f17c8 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( {