-
Notifications
You must be signed in to change notification settings - Fork 10
feat(workflow-executor): serialize recordId as pipe string at front/orchestrator boundaries #1594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aa62e44
ec3c095
662b143
6847d82
13b1bdf
84c7fd8
eabb6ec
b79ad21
62001d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import type { RecordId } from '../types/validated/collection'; | ||
|
|
||
| import { RecordIdSerializationError } from '../errors'; | ||
|
|
||
| export function serializeRecordId(recordId: RecordId): string { | ||
| return recordId | ||
| .map(part => { | ||
| const serialized = String(part); | ||
|
|
||
| if (serialized.includes('|')) { | ||
| throw new RecordIdSerializationError(serialized); | ||
| } | ||
|
|
||
| return serialized; | ||
| }) | ||
| .join('|'); | ||
| } | ||
|
|
||
| export function deserializeRecordId(value: string): string[] { | ||
| return value.split('|'); | ||
|
hercemer42 marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import type { StepExecutionData } from '../types/step-execution-data'; | ||
| import type { RecordRef } from '../types/validated/collection'; | ||
|
|
||
| import { serializeRecordId } from '../adapters/record-id-serializer'; | ||
|
|
||
| function serializeRecordRef(ref: RecordRef): unknown { | ||
| return { ...ref, recordId: serializeRecordId(ref.recordId) }; | ||
| } | ||
|
|
||
| export default function serializeStepForWire(step: StepExecutionData): unknown { | ||
| switch (step.type) { | ||
| case 'read-record': | ||
| case 'update-record': | ||
| case 'trigger-action': | ||
| return { ...step, selectedRecordRef: serializeRecordRef(step.selectedRecordRef) }; | ||
|
|
||
| case 'load-related-record': { | ||
| const result: Record<string, unknown> = { | ||
| ...step, | ||
| selectedRecordRef: serializeRecordRef(step.selectedRecordRef), | ||
| }; | ||
|
|
||
| if (step.pendingData) { | ||
|
hercemer42 marked this conversation as resolved.
|
||
| const { availableRecordIds, suggestedRecord } = step.pendingData; | ||
|
|
||
| result.pendingData = { | ||
| ...step.pendingData, | ||
| availableRecordIds: availableRecordIds.map(c => ({ | ||
| ...c, | ||
| recordId: serializeRecordId(c.recordId), | ||
| })), | ||
| ...(suggestedRecord && { | ||
| suggestedRecord: { | ||
| ...suggestedRecord, | ||
| recordId: serializeRecordId(suggestedRecord.recordId), | ||
| }, | ||
| }), | ||
| }; | ||
| } | ||
|
|
||
| if (step.executionResult && 'record' in step.executionResult) { | ||
| result.executionResult = { | ||
| ...step.executionResult, | ||
| record: serializeRecordRef(step.executionResult.record), | ||
| }; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| default: | ||
| return step; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| import type { RecordId } from '../types/validated/collection'; | ||
|
|
||
| export interface CreateActivityLogArgs { | ||
| renderingId: number; | ||
| action: string; | ||
| type: 'read' | 'write'; | ||
| collectionId?: string; | ||
| recordId?: string | number; | ||
| recordId?: RecordId; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't we still have string | number in activity logs ?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is — the array is only the internal representation. The adapter serializes it to the pipe wire format before sending (forestadmin-client-activity-log-port.ts:39:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RecordId will be formatted when we send the request. |
||
| label?: string; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,12 +94,15 @@ export type CollectionSchema = z.infer<typeof CollectionSchemaSchema>; | |
| export const RecordRefSchema = z | ||
| .object({ | ||
| collectionName: z.string().min(1), | ||
| recordId: z.array(z.union([z.string(), z.number()])).min(1), | ||
| recordId: z.array(z.union([z.string().min(1), z.number()])).min(1), | ||
| // Index of the workflow step that loaded this record. | ||
| stepIndex: z.number().int().nonnegative(), | ||
| }) | ||
| .strict(); | ||
| export type RecordRef = z.infer<typeof RecordRefSchema>; | ||
|
|
||
| // A record's primary key: one segment for a simple key, several for a composite key. | ||
| export type RecordId = RecordRef['recordId']; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it was clearer to leave Array<string | number>
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe just renaming it RecordIdArray is ok
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer to have one source of truth. |
||
|
|
||
| // No stepIndex — the agent doesn't know about steps. | ||
| export type RecordData = Omit<RecordRef, 'stepIndex'> & { values: Record<string, unknown> }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { deserializeRecordId, serializeRecordId } from '../../src/adapters/record-id-serializer'; | ||
| import { RecordIdSerializationError } from '../../src/errors'; | ||
|
|
||
| describe('serializeRecordId', () => { | ||
| it('single id → no pipe', () => { | ||
| expect(serializeRecordId(['42'])).toBe('42'); | ||
| }); | ||
|
|
||
| it('composite ids → pipe-joined', () => { | ||
| expect(serializeRecordId(['id1', 'id2'])).toBe('id1|id2'); | ||
| }); | ||
|
|
||
| it('numbers are stringified', () => { | ||
| expect(serializeRecordId([42, 99])).toBe('42|99'); | ||
| }); | ||
|
|
||
| it('mixed string and number ids', () => { | ||
| expect(serializeRecordId(['org', 42])).toBe('org|42'); | ||
| }); | ||
|
|
||
| // '|' is the reserved segment delimiter, so a part that contains it would over-split on the way | ||
| // back and match the wrong record. Fail loudly instead of silently corrupting the key. | ||
| it('throws when a part contains the reserved pipe separator', () => { | ||
| expect(() => serializeRecordId(['a|b'])).toThrow(RecordIdSerializationError); | ||
| }); | ||
| }); | ||
|
|
||
| describe('deserializeRecordId', () => { | ||
| it('single id → single-element array', () => { | ||
| expect(deserializeRecordId('42')).toEqual(['42']); | ||
| }); | ||
|
|
||
| it('pipe string → multi-element array', () => { | ||
| expect(deserializeRecordId('id1|id2')).toEqual(['id1', 'id2']); | ||
| }); | ||
|
|
||
| it('three segments', () => { | ||
| expect(deserializeRecordId('a|b|c')).toEqual(['a', 'b', 'c']); | ||
| }); | ||
|
|
||
| // deserialize stays permissive (bare split). Empty segments from a malformed wire value are | ||
| // rejected at the validation boundaries that consume the result, not here. | ||
| it('over-splits a raw value containing the reserved pipe (rejected at the boundary, not here)', () => { | ||
| expect(deserializeRecordId('a|b|c')).toEqual(['a', 'b', 'c']); | ||
| expect(deserializeRecordId('a|')).toEqual(['a', '']); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.