Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

📝 following beta format X.Y.Z where Y = breaking change and Z = feature and fix. Later => FAIL.FEATURE.FIX

## 0.15.4(2026-02-17)

- Fix: Plain object in flex field mutation threw an error
- Fix: Array in flex field mutation threw an error
- Tests: Added tests for flex fields with object values

## 0.15.3(2026-02-12)

- Fix: Upgrade SurrealDB SDK to 2.0.0-alpha.18
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blitznocode/blitz-orm",
"version": "0.15.3",
"version": "0.15.4",
"author": "blitznocode.com",
"description": "Blitz-orm is an Object Relational Mapper (ORM) for graph databases that uses a JSON query language called Blitz Query Language (BQL). BQL is similar to GraphQL but uses JSON instead of strings. This makes it easier to build dynamic queries.",
"main": "dist/index.mjs",
Expand Down Expand Up @@ -106,5 +106,5 @@
"directories": {
"test": "tests"
},
"packageManager": "pnpm@8.10.2+sha512.0782093d5ba6c7ad9462081bc1ef0775016a4b4109eca1e1fedcea6f110143af5f50993db36c427d4fa8c62be3920a3224db12da719d246ca19dd9f18048c33c"
"packageManager": "pnpm@10.30.0"
}
3 changes: 3 additions & 0 deletions src/adapters/surrealDB/parsing/parseFlexVal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ export const parseFlexValSurrealDB = (v: unknown) => {
if (['number', 'boolean'].includes(typeof v)) {
return v;
}
if (typeof v === 'object' && v !== null) {
return JSON.stringify(v);
}
throw new Error(`Unsupported type ${typeof v}`);
};
6 changes: 2 additions & 4 deletions src/adapters/surrealDB/parsing/values.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isArray, isDate } from 'radash';
import { isDate } from 'radash';
import { parseFlexValSurrealDB } from './parseFlexVal';

export const surrealDBtypeMap: Record<string, string> = {
Expand Down Expand Up @@ -54,9 +54,7 @@ export const parseValueSurrealDB = (value: unknown, ct?: string): any => {
}
return `<datetime>"${value}"`; //let surrealDB try to do the conversion
case 'FLEX': {
// array elements go throw the parsing
const parsedVal = isArray(value) ? value.map((v) => parseFlexValSurrealDB(v)) : parseFlexValSurrealDB(value);
return `${isArray(parsedVal) ? parsedVal.map((v) => v) : parsedVal}`;
return parseFlexValSurrealDB(value);
}
default:
throw new Error(`Unsupported data field type ${ct}.`);
Expand Down
7 changes: 6 additions & 1 deletion src/stateMachine/mutation/bql/enrich.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { traverse } from 'object-traversal';
import { isArray, isObject } from 'radash';
import { getCurrentFields, getCurrentSchema, getFieldSchema } from '../../../helpers';
import type { BormConfig, BQLMutationBlock, EnrichedBormSchema, EnrichedBQLMutationBlock } from '../../../types';
import { SharedMetadata } from '../../../types/symbols';
import { FlexDataValue, SharedMetadata } from '../../../types/symbols';
import { enrichFilter } from '../../query/bql/enrich';
import { computeFields } from './enrichSteps/computeFields';
import { enrichChildren } from './enrichSteps/enrichChildren';
Expand Down Expand Up @@ -71,6 +71,11 @@ export const enrichBQLMutation = (
return;
}

if (FlexDataValue in value) {
//plain data objects in FLEX ref fields, not mutation nodes
return;
}

if ('$root' in value) {
// This is hte $root object, we will split the real root if needed in this iteration
} else if (!('$thing' in value || '$entity' in value || '$relation' in value)) {
Expand Down
21 changes: 20 additions & 1 deletion src/stateMachine/mutation/bql/enrichSteps/enrichChildren.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ import type {
EnrichedRefField,
EnrichedRoleField,
} from '../../../../types';
import { EdgeSchema, SharedMetadata } from '../../../../types/symbols';
import { EdgeSchema, FlexDataValue, SharedMetadata } from '../../../../types/symbols';
import { get$bzId } from '../shared/get$bzId';
import { getOp } from '../shared/getOp';
import { getOppositePlayers } from '../shared/getOppositePlayers';

const markFlexDataValues = (value: unknown): unknown => {
if (isArray(value)) {
return value.map(markFlexDataValues);
}
if (isObject(value)) {
return { ...(value as Record<string, unknown>), [FlexDataValue]: true };
}
return value;
};

export const enrichChildren = (
node: BQLMutationBlock,
field: string,
Expand All @@ -32,12 +42,21 @@ export const enrichChildren = (

if (!isObject(subNode)) {
if (refSchema.contentType === 'FLEX') {
if (isArray(subNode)) {
// Wrap in array to prevent flatMap from flattening the inner array,
// and recursively mark nested objects with FlexDataValue so the
// traverser in enrich.ts skips them.
return [markFlexDataValues(subNode)];
}
return subNode;
}
throw new Error(`[Wrong format] The refField ${field} must receive an object`);
}

if (!subNode.$thing) {
if (refSchema.contentType === 'FLEX') {
return { ...subNode, [FlexDataValue]: true };
}
throw new Error('[Wrong format] The field $thing is required in refFields');
}
return { ...subNode, $op, $bzId };
Expand Down
14 changes: 13 additions & 1 deletion src/stateMachine/mutation/surql/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,19 @@ export const buildSURQLMutation = async (flat: FlatBqlMutation, schema: Enriched
if (contentType === 'FLEX') {
//todo: card one check len 1
//todo: add/remove etc
return `${rf} = ${cardinality === 'ONE' ? `array::flatten([${block[rf]}])[0]` : `array::flatten([${block[rf]}])`}`;
if (cardinality === 'ONE') {
return `${rf} = array::flatten([${block[rf]}])[0]`;
}
// For MANY: flatten only variable references (entity matches) individually,
// keeping data values (including nested arrays/objects) as-is.
const elements = isArray(block[rf]) ? block[rf] : [block[rf]];
const processedElements = elements.map((el: unknown) => {
if (typeof el === 'string' && (el as string).startsWith('$')) {
return `array::flatten([${el}])[0]`;
}
return el;
});
return `${rf} = [${processedElements.join(', ')}]`;
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Non-variable FLEX values are inserted into the SurQL array without serialization, so object values will stringify to "[object Object]" (and plain strings become unquoted identifiers). Use the FLEX serializer (e.g., parseFlexValSurrealDB) for non-$ elements before joining so object values are preserved.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/stateMachine/mutation/surql/build.ts, line 332:

<comment>Non-variable FLEX values are inserted into the SurQL array without serialization, so object values will stringify to "[object Object]" (and plain strings become unquoted identifiers). Use the FLEX serializer (e.g., parseFlexValSurrealDB) for non-$ elements before joining so object values are preserved.</comment>

<file context>
@@ -317,7 +317,19 @@ export const buildSURQLMutation = async (flat: FlatBqlMutation, schema: Enriched
+          }
+          return el;
+        });
+        return `${rf} = [${processedElements.join(', ')}]`;
       }
 
</file context>
Fix with Cubic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aguspdana wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block[rf] is either a string or a string array. Example of block:

{
  '$thingType': 'relation',
  '$op': 'replace',
  '$bzId': 'R_5WUuSigSeWgZOENaOlbnV',
  '$thing': 'FlexRefRel',
  flexReferences: [ '{"msg":"Hello, world!"}' ],
  [Symbol(stepPrint)]: Set(1) { 'clean' }
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

}

throw new Error(`Unsupported contentType ${contentType}`);
Expand Down
3 changes: 3 additions & 0 deletions src/types/symbols/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const FieldSchema = Symbol.for('fieldSchema');
/// Shared schema metadata
export const SharedMetadata = Symbol.for('sharedMetadata');

/// Marks plain objects stored as data values in FLEX ref fields
export const FlexDataValue = Symbol.for('flexDataValue');

/// SurrealDB schema metadata
export const SuqlMetadata = Symbol.for('suqlMetadata');

Expand Down
74 changes: 74 additions & 0 deletions tests/unit/mutations/refFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1135,4 +1135,78 @@ export const testRefFieldsMutations = createTest('Mutation: RefFields', (ctx) =>
flexReferences: ['hello ? yes : no', 'User:abc:xyz', 'things it can do: jumping', 'User: hey', 'User:hey '],
});
});

it('fl8:[flex, object] Should accept objects in flexReferences', async () => {
const flexWithObject = {
id: 'fl8-flex-with-object',
flexReferences: [{ msg: 'Hello, world!' }],
};
await ctx.mutate(
[
{
...flexWithObject,
$thing: 'FlexRefRel',
// We need to link something when creating a relation to avoid "[Wrong format] Can't create a relation without any player".
space: { id: 'fl8-space', name: 'fl8-space' },
},
],
{ noMetadata: true },
);

const res = await ctx.query(
{
$relation: 'FlexRefRel',
$id: 'fl8-flex-with-object',
$fields: ['id', 'flexReferences'],
},
{ noMetadata: true },
);

//clean
await ctx.mutate({
$thing: 'FlexRefRel',
$op: 'delete',
$id: 'fl8-flex-with-object',
space: { $op: 'delete' },
});

expect(res).toEqual(flexWithObject);
});

it('fl9:[flex, object] Should accept an array of objects in flexReferences', async () => {
const flexWithObject = {
id: 'fl8-flex-with-object',
flexReferences: [[{ msg: 'Hello, world!' }]],
};
await ctx.mutate(
[
{
...flexWithObject,
$thing: 'FlexRefRel',
// We need to link something when creating a relation to avoid "[Wrong format] Can't create a relation without any player".
space: { id: 'fl8-space', name: 'fl8-space' },
},
],
{ noMetadata: true },
);

const res = await ctx.query(
{
$relation: 'FlexRefRel',
$id: 'fl8-flex-with-object',
$fields: ['id', 'flexReferences'],
},
{ noMetadata: true },
);

//clean
await ctx.mutate({
$thing: 'FlexRefRel',
$op: 'delete',
$id: 'fl8-flex-with-object',
space: { $op: 'delete' },
});

expect(res).toEqual(flexWithObject);
});
});