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
19 changes: 19 additions & 0 deletions packages/agent-client/src/domains/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,25 @@ export default class Collection extends CollectionChart {
});
}

// Fetch a single record by its (possibly composite) id via the agent's by-id route. The id is
// serialized opaquely (serializeRecordId), so the agent — the only party that knows the primary
// key column order — does the matching. Returns null when the record does not exist (404, or a
// 200 with an empty payload, which some agents return for a missing composite-key record).
async getOne<Data = unknown>(id: RecordId, options?: SelectOptions): Promise<Data | null> {
try {
const record = await this.httpRequester.query<Data>({
method: 'get',
path: `/forest/${this.name}/${serializeRecordId(id)}`,
query: QuerySerializer.serialize(options, this.name),
});

return record && Object.keys(record as object).length > 0 ? record : null;
} catch (error) {
if ((this.httpRequester.constructor as typeof HttpRequester).is404Error(error)) return null;
throw error;
}
}

private getActionInfo(
actionEndpoints: ActionEndpointsByCollection,
collectionName: string,
Expand Down
71 changes: 20 additions & 51 deletions packages/workflow-executor/src/adapters/agent-client-agent-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import type {
} from '../ports/agent-port';
import type SchemaCache from '../schema-cache';
import type { StepUser } from '../types/execution-context';
import type { CollectionSchema, RecordData } from '../types/validated/collection';
import type { ActionEndpointsByCollection, SelectOptions } from '@forestadmin/agent-client';
import type { RecordData } from '../types/validated/collection';
import type { ActionEndpointsByCollection } from '@forestadmin/agent-client';

import { createRemoteAgentClient } from '@forestadmin/agent-client';
import jsonwebtoken from 'jsonwebtoken';
Expand Down Expand Up @@ -45,24 +45,6 @@ function restoreFieldNames(
return Object.fromEntries(Object.entries(values).map(([k, v]) => [camelToOriginal[k] ?? k, v]));
}

function buildPkFilter(
primaryKeyFields: string[],
id: Array<string | number>,
): SelectOptions['filters'] {
if (primaryKeyFields.length === 1) {
return { field: primaryKeyFields[0], operator: 'Equal', value: id[0] };
}

return {
aggregator: 'And',
conditions: primaryKeyFields.map((field, i) => ({
field,
operator: 'Equal',
value: id[i],
})),
};
}

export default class AgentClientAgentPort implements AgentPort {
private readonly agentUrl: string;
private readonly authSecret: string;
Expand All @@ -77,21 +59,21 @@ export default class AgentClientAgentPort implements AgentPort {
async getRecord({ collection, id, fields }: GetRecordQuery, user: StepUser): Promise<RecordData> {
return this.callAgent('getRecord', async () => {
const client = this.createClient(user);
const schema = this.resolveSchema(collection);
const records = await client.collection(collection).list<Record<string, unknown>>({
filters: buildPkFilter(schema.primaryKeyFields, id),
pagination: { size: 1, number: 1 },
...(fields?.length && { fields }),
});
// Fetch by id through the agent's by-id route (like update/delete): the recordId is an
// opaque ordered token and the agent — the only party that knows the primary key column
// order — does the matching. No buildPkFilter / primaryKeyFields ordering assumption here.
const record = await client
.collection(collection)
.getOne<Record<string, unknown>>(id, { ...(fields?.length && { fields }) });

if (records.length === 0) {
if (!record) {
throw new RecordNotFoundError(collection, id.join('|'));
}

return {
collectionName: collection,
recordId: id,
values: restoreFieldNames(records[0], fields),
values: restoreFieldNames(record, fields),
};
});
}
Expand Down Expand Up @@ -134,9 +116,18 @@ export default class AgentClientAgentPort implements AgentPort {
relatedSchema.fields.map(f => f.fieldName),
);

// For composite PKs, rebuilding the id from primaryKeyFields assumes the schema's
// (alphabetical) order matches the agent's column order — it may not, which would
// mis-pair the key. Use the agent's opaque record id (pipe-joined when composite),
// like getSingleRelatedData, so it round-trips through the by-id route.
const recordId =
relatedSchema.primaryKeyFields.length > 1
? String(row.id).split('|')
: relatedSchema.primaryKeyFields.map(f => restored[f] as string | number);

return {
collectionName: relatedSchema.collectionName,
recordId: relatedSchema.primaryKeyFields.map(f => restored[f] as string | number),
recordId,
values: restored,
};
});
Expand Down Expand Up @@ -289,26 +280,4 @@ export default class AgentClientAgentPort implements AgentPort {

return endpoints;
}

private resolveSchema(collectionName: string): CollectionSchema {
const cached = this.schemaCache.get(collectionName);

if (!cached) {
// eslint-disable-next-line no-console
console.warn(
`[workflow-executor] Schema not found in cache for collection "${collectionName}". ` +
'Falling back to primaryKeyFields: ["id"]. Call getCollectionSchema first.',
);
}

return (
cached ?? {
collectionName,
collectionDisplayName: collectionName,
primaryKeyFields: ['id'],
fields: [],
actions: [],
}
);
}
}
Loading