Skip to content

Commit a8600ca

Browse files
Support for NDC Spec v0.1.0-rc.15 and nested object/array selection (#8)
1 parent a21eb56 commit a8600ca

File tree

10 files changed

+898
-96
lines changed

10 files changed

+898
-96
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ This changelog documents the changes between release versions.
44
## main
55
Changes to be included in the next upcoming release
66

7+
- Support for NDC Spec v0.1.0-rc.15 via the NDC TypeScript SDK v3.0.0. This is a breaking change and must be used with the latest Hasura engine.
8+
- Support for nested object/array selection
9+
- New function calling convention that relies on nested object queries
10+
- New mutation request/response format
11+
712
## v0.14.0
813
- Support for "relaxed types" ([#10](https://github.com/hasura/ndc-nodejs-lambda/pull/10))
914

ndc-lambda-sdk/package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ndc-lambda-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"url": "git+https://github.com/hasura/ndc-nodejs-lambda.git"
3131
},
3232
"dependencies": {
33-
"@hasura/ndc-sdk-typescript": "^1.2.8",
33+
"@hasura/ndc-sdk-typescript": "^3.0.0",
3434
"@json-schema-tools/meta-schema": "^1.7.0",
3535
"@tsconfig/node18": "^18.2.2",
3636
"commander": "^11.1.0",

ndc-lambda-sdk/src/cmdline.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export function makeCommand(commandActions: CommandActions): Command {
1717
.name("ndc-lambda-sdk")
1818
.version(version);
1919

20-
const serveCommand = sdk.get_serve_command();
20+
const serveCommand = sdk.getServeCommand();
2121
serveCommand.action((serverOptions: sdk.ServerOptions, command: Command) => {
2222
const hostOpts: HostOptions = hostCommand.opts();
2323
return commandActions.serveAction(hostOpts, serverOptions);
2424
})
2525

26-
const configurationServeCommand = sdk.get_serve_configuration_command();
26+
const configurationServeCommand = sdk.getServeConfigurationCommand();
2727
configurationServeCommand.commands.find(c => c.name() === "serve")?.action((serverOptions: sdk.ConfigurationServerOptions, command: Command) => {
2828
const hostOpts: HostOptions = hostCommand.opts();
2929
return commandActions.configurationServeAction(hostOpts, serverOptions);

ndc-lambda-sdk/src/connector.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type State = {
1515
}
1616

1717
export const RAW_CONFIGURATION_SCHEMA: JSONSchemaObject = {
18-
description: 'NodeJS Functions SDK Connector Configuration',
18+
description: 'NodeJS Lambda SDK Connector Configuration',
1919
type: 'object',
2020
required: [],
2121
properties: {}
@@ -29,19 +29,19 @@ export function createConnector(options: ConnectorOptions): sdk.Connector<RawCon
2929
const functionsFilePath = path.resolve(options.functionsFilePath);
3030

3131
const connector: sdk.Connector<RawConfiguration, Configuration, State> = {
32-
get_raw_configuration_schema: function (): JSONSchemaObject {
32+
getRawConfigurationSchema: function (): JSONSchemaObject {
3333
return RAW_CONFIGURATION_SCHEMA;
3434
},
3535

36-
make_empty_configuration: function (): RawConfiguration {
36+
makeEmptyConfiguration: function (): RawConfiguration {
3737
return {};
3838
},
3939

40-
update_configuration: async function (rawConfiguration: RawConfiguration): Promise<RawConfiguration> {
40+
updateConfiguration: async function (rawConfiguration: RawConfiguration): Promise<RawConfiguration> {
4141
return {};
4242
},
4343

44-
validate_raw_configuration: async function (rawConfiguration: RawConfiguration): Promise<Configuration> {
44+
validateRawConfiguration: async function (rawConfiguration: RawConfiguration): Promise<Configuration> {
4545
const schemaResults = deriveSchema(functionsFilePath);
4646
printCompilerDiagnostics(schemaResults.compilerDiagnostics);
4747
printFunctionIssues(schemaResults.functionIssues);
@@ -51,7 +51,7 @@ export function createConnector(options: ConnectorOptions): sdk.Connector<RawCon
5151
}
5252
},
5353

54-
try_init_state: async function (configuration: Configuration, metrics: unknown): Promise<State> {
54+
tryInitState: async function (configuration: Configuration, metrics: unknown): Promise<State> {
5555
if (Object.keys(configuration.functionsSchema.functions).length === 0) {
5656
// If there are no declared functions, don't bother trying to load the code.
5757
// There's very likely to be compiler errors during schema inference that will
@@ -61,18 +61,19 @@ export function createConnector(options: ConnectorOptions): sdk.Connector<RawCon
6161
return { runtimeFunctions: require(functionsFilePath) }
6262
},
6363

64-
get_capabilities: function (configuration: Configuration): sdk.CapabilitiesResponse {
64+
getCapabilities: function (configuration: Configuration): sdk.CapabilitiesResponse {
6565
return {
66-
versions: "^0.1.0",
66+
version: "0.1.0",
6767
capabilities: {
6868
query: {
6969
variables: {}
7070
},
71+
mutation: {},
7172
}
7273
};
7374
},
7475

75-
get_schema: async function (configuration: Configuration): Promise<sdk.SchemaResponse> {
76+
getSchema: async function (configuration: Configuration): Promise<sdk.SchemaResponse> {
7677
return getNdcSchema(configuration.functionsSchema);
7778
},
7879

@@ -84,15 +85,19 @@ export function createConnector(options: ConnectorOptions): sdk.Connector<RawCon
8485
return await executeMutation(request, configuration.functionsSchema, state.runtimeFunctions);
8586
},
8687

87-
explain: function (configuration: Configuration, state: State, request: sdk.QueryRequest): Promise<sdk.ExplainResponse> {
88+
queryExplain: function (configuration: Configuration, state: State, request: sdk.QueryRequest): Promise<sdk.ExplainResponse> {
8889
throw new Error("Function not implemented.");
8990
},
9091

91-
health_check: async function (configuration: Configuration, state: State): Promise<undefined> {
92+
mutationExplain: function (configuration: Configuration, state: State, request: sdk.MutationRequest): Promise<sdk.ExplainResponse> {
93+
throw new Error("Function not implemented.");
94+
},
95+
96+
healthCheck: async function (configuration: Configuration, state: State): Promise<undefined> {
9297
return undefined;
9398
},
9499

95-
fetch_metrics: async function (configuration: Configuration, state: State): Promise<undefined> {
100+
fetchMetrics: async function (configuration: Configuration, state: State): Promise<undefined> {
96101
return undefined;
97102
},
98103
}

ndc-lambda-sdk/src/execution.ts

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,23 @@ export async function executeQuery(queryRequest: sdk.QueryRequest, functionsSche
3434
const parallelLimit = pLimit(functionDefinition.parallelDegree ?? DEFAULT_PARALLEL_DEGREE);
3535
const functionInvocations: Promise<sdk.RowSet>[] = functionInvocationPreparedArgs.map(invocationPreparedArgs => parallelLimit(async () => {
3636
const result = await invokeFunction(runtimeFunction, invocationPreparedArgs, functionName);
37-
const prunedResult = reshapeResultToNdcResponseValue(result, functionDefinition.resultType, [], queryRequest.query.fields ?? {}, functionsSchema.objectTypes);
38-
return {
39-
aggregates: {},
40-
rows: [
41-
{
42-
__value: prunedResult
43-
}
44-
]
45-
};
37+
return reshapeResultUsingFunctionCallingConvention(result, functionDefinition.resultType, queryRequest.query, functionsSchema.objectTypes);
4638
}));
4739

4840
return await Promise.all(functionInvocations);
4941
}
5042

5143
export async function executeMutation(mutationRequest: sdk.MutationRequest, functionsSchema: schema.FunctionsSchema, runtimeFunctions: RuntimeFunctions): Promise<sdk.MutationResponse> {
52-
const operationResults: sdk.MutationOperationResults[] = [];
44+
if (mutationRequest.operations.length > 1)
45+
throw new sdk.NotSupported("Transactional mutations (multiple operations) are not supported");
46+
if (mutationRequest.operations.length <= 0)
47+
throw new sdk.BadRequest("One mutation operation must be provided")
5348

54-
for (const mutationOperation of mutationRequest.operations) {
55-
const result = await executeMutationOperation(mutationOperation, functionsSchema, runtimeFunctions);
56-
operationResults.push(result);
57-
}
49+
const mutationOperation = mutationRequest.operations[0]!;
50+
const result = await executeMutationOperation(mutationOperation, functionsSchema, runtimeFunctions);
5851

5952
return {
60-
operation_results: operationResults
53+
operation_results: [result]
6154
};
6255
}
6356

@@ -77,13 +70,11 @@ async function executeMutationOperation(mutationOperation: sdk.MutationOperation
7770

7871
const preparedArgs = prepareArguments(mutationOperation.arguments, functionDefinition, functionsSchema.objectTypes);
7972
const result = await invokeFunction(runtimeFunction, preparedArgs, functionName);
80-
const prunedResult = reshapeResultToNdcResponseValue(result, functionDefinition.resultType, [], mutationOperation.fields ?? {}, functionsSchema.objectTypes);
73+
const reshapedResult = reshapeResultUsingFieldSelection(result, functionDefinition.resultType, [], mutationOperation.fields ?? { type: "scalar" }, functionsSchema.objectTypes);
8174

8275
return {
83-
affected_rows: 1,
84-
returning: [{
85-
__value: prunedResult
86-
}]
76+
type: "procedure",
77+
result: reshapedResult
8778
}
8879
}
8980

@@ -211,20 +202,76 @@ function buildCausalStackTrace(error: Error): string {
211202
return stackTrace;
212203
}
213204

214-
export function reshapeResultToNdcResponseValue(value: unknown, type: schema.TypeReference, valuePath: string[], fields: Record<string, sdk.Field> | "AllColumns", objectTypes: schema.ObjectTypeDefinitions): unknown {
205+
// Represents either selecting a scalar (ie. the whole value, opaquely), an object (selecting properties), or an array (select whole array)
206+
export type FieldSelection = sdk.NestedField | { type: "scalar" }
207+
208+
function reshapeResultUsingFunctionCallingConvention(functionResultValue: unknown, functionResultType: schema.TypeReference, query: sdk.Query, objectTypes: schema.ObjectTypeDefinitions): sdk.RowSet {
209+
if (query.aggregates) throw new sdk.NotSupported("Query aggregates are not supported");
210+
if (query.order_by) throw new sdk.NotSupported("Query order_by is not supported");
211+
if (query.predicate) throw new sdk.NotSupported("Query predicate is not supported");
212+
if (!query.fields) {
213+
return {
214+
aggregates: null,
215+
rows: null,
216+
}
217+
}
218+
// There's one virtual row in the function calling convention, so if the query (pointlessly) usees
219+
// pagination to skip it, just do what it says
220+
if (query.limit !== undefined && query.limit !== null && query.limit <= 0
221+
|| query.offset !== undefined && query.offset !== null && query.offset >= 1) {
222+
return {
223+
aggregates: null,
224+
rows: [],
225+
}
226+
}
227+
228+
const rowValue = mapObjectValues(query.fields, (field: sdk.Field, fieldName: string) => {
229+
switch (field.type) {
230+
case "column":
231+
if (field.column === "__value") {
232+
return reshapeResultUsingFieldSelection(functionResultValue, functionResultType, [fieldName], field.fields ?? { type: "scalar" }, objectTypes);
233+
} else {
234+
throw new sdk.BadRequest(`Unknown column '${field.column}' used in root query field`)
235+
}
236+
237+
case "relationship":
238+
throw new sdk.NotSupported(`Field '${fieldName}' is a relationship field, which is unsupported.'`)
239+
240+
default:
241+
return unreachable(field["type"]);
242+
}
243+
});
244+
245+
return {
246+
aggregates: null,
247+
rows: [rowValue]
248+
}
249+
}
250+
251+
export function reshapeResultUsingFieldSelection(value: unknown, type: schema.TypeReference, valuePath: string[], fieldSelection: FieldSelection, objectTypes: schema.ObjectTypeDefinitions): unknown {
215252
switch (type.type) {
216253
case "array":
217-
if (isArray(value)) {
218-
return value.map((elementValue, index) => reshapeResultToNdcResponseValue(elementValue, type.elementType, [...valuePath, `[${index}]`], fields, objectTypes))
219-
}
220-
break;
254+
if (!isArray(value))
255+
throw new sdk.InternalServerError(`Expected an array, but received '${value === null ? "null" : null ?? typeof value}'`);
256+
257+
const elementFieldSelection = (() => {
258+
switch (fieldSelection.type) {
259+
case "scalar": return fieldSelection;
260+
case "array": return fieldSelection.fields;
261+
case "object": throw new sdk.BadRequest(`Trying to perform an object selection on an array type at '${valuePath.join(".")}'`)
262+
default: return unreachable(fieldSelection["type"]);
263+
}
264+
})();
265+
266+
return value.map((elementValue, index) => reshapeResultUsingFieldSelection(elementValue, type.elementType, [...valuePath, `[${index}]`], elementFieldSelection, objectTypes))
267+
221268

222269
case "nullable":
223270
// Selected fields must always return a value, so they cannot be undefined. So all
224271
// undefineds are coerced to nulls so that the field is included with a null value.
225272
return value === null || value === undefined
226273
? null
227-
: reshapeResultToNdcResponseValue(value, type.underlyingType, valuePath, fields, objectTypes);
274+
: reshapeResultUsingFieldSelection(value, type.underlyingType, valuePath, fieldSelection, objectTypes);
228275

229276
case "named":
230277
switch (type.kind) {
@@ -240,23 +287,30 @@ export function reshapeResultToNdcResponseValue(value: unknown, type: schema.Typ
240287
if (value === null || Array.isArray(value) || typeof value !== "object")
241288
throw new sdk.InternalServerError(`Expected an object, but received '${value === null ? "null" : null ?? Array.isArray(value) ? "array" : null ?? typeof value}'`);
242289

243-
const selectedFields: Record<string, sdk.Field> =
244-
fields === "AllColumns"
245-
? Object.fromEntries(objectType.properties.map(propDef => [propDef.propertyName, { type: "column", column: propDef.propertyName }]))
246-
: fields;
290+
const selectedFields: Record<string, sdk.Field> = (() => {
291+
switch (fieldSelection.type) {
292+
case "scalar": return Object.fromEntries(objectType.properties.map(propDef => [propDef.propertyName, { type: "column", column: propDef.propertyName }]));
293+
case "array": throw new sdk.BadRequest(`Trying to perform an array selection on an object type at '${valuePath.join(".")}'`);
294+
case "object": return fieldSelection.fields;
295+
default: return unreachable(fieldSelection["type"]);
296+
}
297+
})();
247298

248299
return mapObjectValues(selectedFields, (field, fieldName) => {
249300
switch(field.type) {
250301
case "column":
251302
const objPropDef = objectType.properties.find(prop => prop.propertyName === field.column);
252303
if (objPropDef === undefined)
253-
throw new sdk.InternalServerError(`Unable to find property definition '${field.column}' on object type '${type.name}'`);
304+
throw new sdk.BadRequest(`Unable to find property definition '${field.column}' on object type '${type.name}' at '${valuePath.join(".")}'`);
305+
306+
const columnFieldSelection = field.fields ?? { type: "scalar" };
307+
return reshapeResultUsingFieldSelection((value as Record<string, unknown>)[field.column], objPropDef.type, [...valuePath, fieldName], columnFieldSelection, objectTypes)
254308

255-
// We pass "AllColumns" as the fields because we don't yet support nested field selections, so we just include all columns by default for now
256-
return reshapeResultToNdcResponseValue((value as Record<string, unknown>)[field.column], objPropDef.type, [...valuePath, field.column], "AllColumns", objectTypes)
309+
case "relationship":
310+
throw new sdk.NotSupported(`Field '${fieldName}' is a relationship field, which is unsupported.'`)
257311

258312
default:
259-
throw new sdk.NotSupported(`Field '${fieldName}' uses an unsupported field type: '${field.type}'`)
313+
return unreachable(field["type"]);
260314
}
261315
})
262316

ndc-lambda-sdk/src/host.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { createConnector } from "./connector";
33
import { makeCommand } from "./cmdline";
44

55
const program = makeCommand({
6-
serveAction: (hostOpts, serveOpts) => sdk.start_server(createConnector({functionsFilePath: hostOpts.functions}), serveOpts),
7-
configurationServeAction: (hostOpts, serveOpts) => sdk.start_configuration_server(createConnector({functionsFilePath: hostOpts.functions}), serveOpts),
6+
serveAction: (hostOpts, serveOpts) => sdk.startServer(createConnector({functionsFilePath: hostOpts.functions}), serveOpts),
7+
configurationServeAction: (hostOpts, serveOpts) => sdk.startConfigurationServer(createConnector({functionsFilePath: hostOpts.functions}), serveOpts),
88
});
99

1010
program.parseAsync().catch(err => {

0 commit comments

Comments
 (0)