@teamkeel/functions-runtime 0.411.0 → 0.412.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/index.d.mts +340 -0
  2. package/dist/index.d.ts +340 -0
  3. package/dist/index.js +3093 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +3097 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +23 -5
  8. package/.env.test +0 -2
  9. package/compose.yaml +0 -10
  10. package/src/Duration.js +0 -40
  11. package/src/Duration.test.js +0 -34
  12. package/src/File.js +0 -295
  13. package/src/ModelAPI.js +0 -377
  14. package/src/ModelAPI.test.js +0 -1428
  15. package/src/QueryBuilder.js +0 -184
  16. package/src/QueryContext.js +0 -90
  17. package/src/RequestHeaders.js +0 -21
  18. package/src/TimePeriod.js +0 -89
  19. package/src/TimePeriod.test.js +0 -148
  20. package/src/applyAdditionalQueryConstraints.js +0 -22
  21. package/src/applyJoins.js +0 -67
  22. package/src/applyWhereConditions.js +0 -124
  23. package/src/auditing.js +0 -110
  24. package/src/auditing.test.js +0 -330
  25. package/src/camelCasePlugin.js +0 -52
  26. package/src/casing.js +0 -54
  27. package/src/casing.test.js +0 -56
  28. package/src/consts.js +0 -14
  29. package/src/database.js +0 -244
  30. package/src/errors.js +0 -160
  31. package/src/handleJob.js +0 -110
  32. package/src/handleJob.test.js +0 -270
  33. package/src/handleRequest.js +0 -153
  34. package/src/handleRequest.test.js +0 -463
  35. package/src/handleRoute.js +0 -112
  36. package/src/handleSubscriber.js +0 -105
  37. package/src/index.d.ts +0 -317
  38. package/src/index.js +0 -38
  39. package/src/parsing.js +0 -113
  40. package/src/parsing.test.js +0 -140
  41. package/src/permissions.js +0 -77
  42. package/src/permissions.test.js +0 -118
  43. package/src/tracing.js +0 -184
  44. package/src/tracing.test.js +0 -147
  45. package/src/tryExecuteFunction.js +0 -91
  46. package/src/tryExecuteJob.js +0 -29
  47. package/src/tryExecuteSubscriber.js +0 -17
  48. package/src/type-utils.js +0 -18
  49. package/vite.config.js +0 -7
@@ -1,463 +0,0 @@
1
- import { createJSONRPCRequest, JSONRPCErrorCode } from "json-rpc-2.0";
2
- import { sql } from "kysely";
3
- import { handleRequest, RuntimeErrors } from "./handleRequest";
4
- import { test, expect, beforeEach, describe } from "vitest";
5
- import { ModelAPI } from "./ModelAPI";
6
- import { useDatabase } from "./database";
7
- const { Permissions } = require("./permissions");
8
- import { PROTO_ACTION_TYPES } from "./consts";
9
- import KSUID from "ksuid";
10
- import { ErrorPresets } from "./errors";
11
-
12
- test("when the custom function returns expected value", async () => {
13
- const config = {
14
- functions: {
15
- createPost: async (ctx, inputs) => {
16
- new Permissions().allow();
17
-
18
- return {
19
- title: "a post",
20
- id: "abcde",
21
- };
22
- },
23
- },
24
- actionTypes: {
25
- createPost: PROTO_ACTION_TYPES.CREATE,
26
- },
27
- createContextAPI: () => {
28
- return {
29
- response: {
30
- headers: new Headers(),
31
- },
32
- };
33
- },
34
- };
35
-
36
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
37
-
38
- expect(await handleRequest(rpcReq, config)).toEqual({
39
- id: "123",
40
- jsonrpc: "2.0",
41
- meta: {
42
- headers: {},
43
- },
44
- result: {
45
- title: "a post",
46
- id: "abcde",
47
- },
48
- });
49
- });
50
-
51
- test("when there is no matching function for the path", async () => {
52
- const config = {
53
- functions: {
54
- createPost: async (ctx, inputs) => {},
55
- },
56
- actionTypes: {
57
- createPost: PROTO_ACTION_TYPES.CREATE,
58
- },
59
- createContextAPI: () => {
60
- return {
61
- response: {
62
- headers: new Headers(),
63
- },
64
- };
65
- },
66
- };
67
-
68
- const rpcReq = createJSONRPCRequest("123", "unknown", { title: "a post" });
69
-
70
- expect(await handleRequest(rpcReq, config)).toEqual({
71
- id: "123",
72
- jsonrpc: "2.0",
73
- error: {
74
- code: JSONRPCErrorCode.MethodNotFound,
75
- message: "no corresponding function found for 'unknown'",
76
- },
77
- });
78
- });
79
-
80
- test("when there is an unexpected error in the custom function", async () => {
81
- const config = {
82
- functions: {
83
- createPost: async (ctx, inputs) => {
84
- throw new Error("oopsie daisy");
85
- },
86
- },
87
- actionTypes: {
88
- createPost: PROTO_ACTION_TYPES.CREATE,
89
- },
90
- createContextAPI: () => {
91
- return {
92
- response: {
93
- headers: new Headers(),
94
- },
95
- };
96
- },
97
- };
98
-
99
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
100
-
101
- expect(await handleRequest(rpcReq, config)).toEqual({
102
- id: "123",
103
- jsonrpc: "2.0",
104
- error: {
105
- code: RuntimeErrors.UnknownError,
106
- message: "oopsie daisy",
107
- },
108
- });
109
- });
110
-
111
- test("when a role based permission has already been granted by the main runtime", async () => {
112
- const config = {
113
- functions: {
114
- createPost: async (ctx, inputs, api) => {
115
- return {
116
- title: inputs.title,
117
- };
118
- },
119
- },
120
- actionTypes: {
121
- createPost: PROTO_ACTION_TYPES.CREATE,
122
- },
123
- createModelAPI: () => {},
124
- createContextAPI: () => {
125
- return {
126
- response: {
127
- headers: new Headers(),
128
- },
129
- };
130
- },
131
- };
132
-
133
- let rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
134
-
135
- Object.assign(rpcReq, {
136
- ...rpcReq,
137
- meta: { permissionState: { status: "granted", reason: "role" } },
138
- });
139
- expect(await handleRequest(rpcReq, config)).toEqual({
140
- id: "123",
141
- jsonrpc: "2.0",
142
- result: {
143
- title: "a post",
144
- },
145
- meta: {
146
- headers: {},
147
- },
148
- });
149
- });
150
-
151
- test("when there is an unexpected object thrown in the custom function", async () => {
152
- const config = {
153
- functions: {
154
- createPost: async (ctx, inputs) => {
155
- throw { err: "oopsie daisy" };
156
- },
157
- },
158
- actionTypes: {
159
- createPost: PROTO_ACTION_TYPES.CREATE,
160
- },
161
- createContextAPI: () => {
162
- return {
163
- response: {
164
- headers: new Headers(),
165
- },
166
- };
167
- },
168
- };
169
-
170
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
171
-
172
- expect(await handleRequest(rpcReq, config)).toEqual({
173
- id: "123",
174
- jsonrpc: "2.0",
175
- error: {
176
- code: RuntimeErrors.UnknownError,
177
- message: '{"err":"oopsie daisy"}',
178
- },
179
- });
180
- });
181
-
182
- test("when a NotFound error preset is thrown in the custom function", async () => {
183
- const config = {
184
- functions: {
185
- createPost: async (ctx, inputs) => {
186
- throw new ErrorPresets.NotFound("not here");
187
- },
188
- },
189
- actionTypes: {
190
- createPost: PROTO_ACTION_TYPES.CREATE,
191
- },
192
- createContextAPI: () => {
193
- return {
194
- response: {
195
- headers: new Headers(),
196
- },
197
- };
198
- },
199
- };
200
-
201
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
202
-
203
- expect(await handleRequest(rpcReq, config)).toEqual({
204
- id: "123",
205
- jsonrpc: "2.0",
206
- error: {
207
- code: RuntimeErrors.RecordNotFoundError,
208
- message: "not here",
209
- },
210
- });
211
- });
212
-
213
- test("when a NotFound error preset is returned in the custom function", async () => {
214
- const config = {
215
- functions: {
216
- createPost: async (ctx, inputs) => {
217
- new Permissions().allow();
218
- return new ErrorPresets.NotFound("not here");
219
- },
220
- },
221
- actionTypes: {
222
- createPost: PROTO_ACTION_TYPES.CREATE,
223
- },
224
- createContextAPI: () => {
225
- return {
226
- response: {
227
- headers: new Headers(),
228
- },
229
- };
230
- },
231
- };
232
-
233
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
234
-
235
- expect(await handleRequest(rpcReq, config)).toEqual({
236
- id: "123",
237
- jsonrpc: "2.0",
238
- error: {
239
- code: RuntimeErrors.RecordNotFoundError,
240
- message: "not here",
241
- },
242
- });
243
- });
244
-
245
- test("when a BadRequest error preset is thrown in the custom function", async () => {
246
- const config = {
247
- functions: {
248
- createPost: async (ctx, inputs) => {
249
- throw new ErrorPresets.BadRequest("invalid inputs");
250
- },
251
- },
252
- actionTypes: {
253
- createPost: PROTO_ACTION_TYPES.CREATE,
254
- },
255
- createContextAPI: () => {
256
- return {
257
- response: {
258
- headers: new Headers(),
259
- },
260
- };
261
- },
262
- };
263
-
264
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
265
-
266
- expect(await handleRequest(rpcReq, config)).toEqual({
267
- id: "123",
268
- jsonrpc: "2.0",
269
- error: {
270
- code: RuntimeErrors.BadRequestError,
271
- message: "invalid inputs",
272
- },
273
- });
274
- });
275
-
276
- // The following tests assert on the various
277
- // jsonrpc responses that *should* happen when a user
278
- // writes a custom function that inadvertently causes a pg constraint error to occur inside of our ModelAPI class instance.
279
- describe("ModelAPI error handling", () => {
280
- let functionConfig;
281
- let db;
282
-
283
- beforeEach(async () => {
284
- db = useDatabase();
285
-
286
- await sql`
287
- DROP TABLE IF EXISTS post;
288
- DROP TABLE IF EXISTS author;
289
-
290
- CREATE TABLE author(
291
- "id" text PRIMARY KEY,
292
- "name" text NOT NULL
293
- );
294
-
295
- CREATE TABLE post(
296
- "id" text PRIMARY KEY,
297
- "title" text NOT NULL UNIQUE,
298
- "author_id" text NOT NULL REFERENCES author(id)
299
- );
300
- `.execute(db);
301
-
302
- await sql`
303
- INSERT INTO author (id, name) VALUES ('adam', 'adam bull')
304
- `.execute(db);
305
-
306
- const models = {
307
- post: new ModelAPI("post", undefined, {
308
- post: {
309
- author: {
310
- relationshipType: "belongsTo",
311
- foreignKey: "author_id",
312
- referencesTable: "person",
313
- },
314
- },
315
- }),
316
- };
317
-
318
- functionConfig = {
319
- permissionFns: {},
320
- actionTypes: {
321
- createPost: PROTO_ACTION_TYPES.CREATE,
322
- deletePost: PROTO_ACTION_TYPES.DELETE,
323
- },
324
- functions: {
325
- createPost: async (ctx, inputs) => {
326
- new Permissions().allow();
327
-
328
- const post = await models.post.create({
329
- id: KSUID.randomSync().string,
330
- ...inputs,
331
- });
332
-
333
- return post;
334
- },
335
- deletePost: async (ctx, inputs) => {
336
- new Permissions().allow();
337
-
338
- const deleted = await models.post.delete(inputs);
339
-
340
- return deleted;
341
- },
342
- },
343
- createContextAPI: () => {
344
- return {
345
- response: {
346
- headers: new Headers(),
347
- },
348
- };
349
- },
350
- };
351
- });
352
-
353
- test("when kysely returns a no result error", async () => {
354
- // a kysely NoResultError is thrown when attempting to delete/update a non existent record.
355
- const rpcReq = createJSONRPCRequest("123", "deletePost", {
356
- id: "non-existent-id",
357
- });
358
-
359
- expect(await handleRequest(rpcReq, functionConfig)).toEqual({
360
- id: "123",
361
- jsonrpc: "2.0",
362
- error: {
363
- code: RuntimeErrors.RecordNotFoundError,
364
- message: "",
365
- },
366
- });
367
- });
368
-
369
- test("when there is a not null constraint error", async () => {
370
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: null });
371
-
372
- expect(await handleRequest(rpcReq, functionConfig)).toEqual({
373
- id: "123",
374
- jsonrpc: "2.0",
375
- error: {
376
- code: RuntimeErrors.NotNullConstraintError,
377
- message:
378
- 'null value in column "title" of relation "post" violates not-null constraint',
379
- data: {
380
- code: "23502",
381
- column: "title",
382
- detail: expect.stringContaining("Failing row contains"),
383
- table: "post",
384
- value: undefined,
385
- },
386
- },
387
- });
388
- });
389
- test("when there is a uniqueness constraint error", async () => {
390
- await sql`
391
- INSERT INTO post (id, title, author_id) values(${
392
- KSUID.randomSync().string
393
- }, 'hello', 'adam')
394
- `.execute(db);
395
-
396
- const rpcReq = createJSONRPCRequest("123", "createPost", {
397
- title: "hello",
398
- author_id: "something",
399
- });
400
-
401
- expect(await handleRequest(rpcReq, functionConfig)).toEqual({
402
- id: "123",
403
- jsonrpc: "2.0",
404
- error: {
405
- code: RuntimeErrors.UniqueConstraintError,
406
- message:
407
- 'duplicate key value violates unique constraint "post_title_key"',
408
- data: {
409
- code: "23505",
410
- column: "title",
411
- detail: "Key (title)=(hello) already exists.",
412
- table: "post",
413
- value: "hello",
414
- },
415
- },
416
- });
417
- });
418
-
419
- test("when there is a null value in a foreign key column", async () => {
420
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "123" });
421
-
422
- expect(await handleRequest(rpcReq, functionConfig)).toEqual({
423
- id: "123",
424
- jsonrpc: "2.0",
425
- error: {
426
- code: RuntimeErrors.NotNullConstraintError,
427
- message:
428
- 'null value in column "author_id" of relation "post" violates not-null constraint',
429
- data: {
430
- code: "23502",
431
- column: "author_id",
432
- detail: expect.stringContaining("Failing row contains"),
433
- table: "post",
434
- value: undefined,
435
- },
436
- },
437
- });
438
- });
439
-
440
- test("when there is a foreign key constraint violation", async () => {
441
- const rpcReq2 = createJSONRPCRequest("123", "createPost", {
442
- title: "123",
443
- author_id: "fake",
444
- });
445
-
446
- expect(await handleRequest(rpcReq2, functionConfig)).toEqual({
447
- id: "123",
448
- jsonrpc: "2.0",
449
- error: {
450
- code: RuntimeErrors.ForeignKeyConstraintError,
451
- message:
452
- 'insert or update on table "post" violates foreign key constraint "post_author_id_fkey"',
453
- data: {
454
- code: "23503",
455
- column: "author_id",
456
- detail: 'Key (author_id)=(fake) is not present in table "author".',
457
- table: "post",
458
- value: "fake",
459
- },
460
- },
461
- });
462
- });
463
- });
@@ -1,112 +0,0 @@
1
- const {
2
- createJSONRPCErrorResponse,
3
- createJSONRPCSuccessResponse,
4
- JSONRPCErrorCode,
5
- } = require("json-rpc-2.0");
6
- const { createDatabaseClient, withDatabase } = require("./database");
7
- const { withAuditContext } = require("./auditing");
8
- const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
9
- const opentelemetry = require("@opentelemetry/api");
10
- const { withSpan } = require("./tracing");
11
-
12
- async function handleRoute(request, config) {
13
- // Try to extract trace context from caller
14
- const activeContext = opentelemetry.propagation.extract(
15
- opentelemetry.context.active(),
16
- request.meta?.tracing
17
- );
18
-
19
- // Run the whole request with the extracted context
20
- return opentelemetry.context.with(activeContext, () => {
21
- // Wrapping span for the whole request
22
- return withSpan(request.method, async (span) => {
23
- let db = null;
24
-
25
- try {
26
- const { createContextAPI, functions } = config;
27
-
28
- if (!(request.method in functions)) {
29
- const message = `no route function found for '${request.method}'`;
30
- span.setStatus({
31
- code: opentelemetry.SpanStatusCode.ERROR,
32
- message: message,
33
- });
34
- return createJSONRPCErrorResponse(
35
- request.id,
36
- JSONRPCErrorCode.MethodNotFound,
37
- message
38
- );
39
- }
40
-
41
- // For route functions context doesn't need request headers or the response object as this is handled by
42
- // params and the function response respectively
43
- const {
44
- headers,
45
- response: __,
46
- ...ctx
47
- } = createContextAPI({
48
- responseHeaders: new Headers(),
49
- meta: request.meta,
50
- });
51
-
52
- // Add request headers to params
53
- request.params.headers = headers;
54
-
55
- db = createDatabaseClient({
56
- connString: request.meta?.secrets?.KEEL_DB_CONN,
57
- });
58
- const routeHandler = functions[request.method];
59
-
60
- const result = await withDatabase(db, false, () => {
61
- return withAuditContext(request, () => {
62
- return routeHandler(request.params, ctx);
63
- });
64
- });
65
-
66
- if (result instanceof Error) {
67
- span.recordException(result);
68
- span.setStatus({
69
- code: opentelemetry.SpanStatusCode.ERROR,
70
- message: result.message,
71
- });
72
- return errorToJSONRPCResponse(request, result);
73
- }
74
-
75
- const response = createJSONRPCSuccessResponse(request.id, result);
76
-
77
- return response;
78
- } catch (e) {
79
- if (e instanceof Error) {
80
- span.recordException(e);
81
- span.setStatus({
82
- code: opentelemetry.SpanStatusCode.ERROR,
83
- message: e.message,
84
- });
85
- return errorToJSONRPCResponse(request, e);
86
- }
87
-
88
- const message = JSON.stringify(e);
89
-
90
- span.setStatus({
91
- code: opentelemetry.SpanStatusCode.ERROR,
92
- message: message,
93
- });
94
-
95
- return createJSONRPCErrorResponse(
96
- request.id,
97
- RuntimeErrors.UnknownError,
98
- message
99
- );
100
- } finally {
101
- if (db) {
102
- await db.destroy();
103
- }
104
- }
105
- });
106
- });
107
- }
108
-
109
- module.exports = {
110
- handleRoute,
111
- RuntimeErrors,
112
- };
@@ -1,105 +0,0 @@
1
- const {
2
- createJSONRPCErrorResponse,
3
- createJSONRPCSuccessResponse,
4
- JSONRPCErrorCode,
5
- } = require("json-rpc-2.0");
6
- const { createDatabaseClient } = require("./database");
7
- const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
8
- const opentelemetry = require("@opentelemetry/api");
9
- const { withSpan } = require("./tracing");
10
- const { PROTO_ACTION_TYPES } = require("./consts");
11
- const { tryExecuteSubscriber } = require("./tryExecuteSubscriber");
12
- const { parseInputs } = require("./parsing");
13
-
14
- // Generic handler function that is agnostic to runtime environment (local or lambda)
15
- // to execute a subscriber function based on the contents of a jsonrpc-2.0 payload object.
16
- // To read more about jsonrpc request and response shapes, please read https://www.jsonrpc.org/specification
17
- async function handleSubscriber(request, config) {
18
- // Try to extract trace context from caller
19
- const activeContext = opentelemetry.propagation.extract(
20
- opentelemetry.context.active(),
21
- request.meta?.tracing
22
- );
23
-
24
- // Run the whole request with the extracted context
25
- return opentelemetry.context.with(activeContext, () => {
26
- // Wrapping span for the whole request
27
- return withSpan(request.method, async (span) => {
28
- let db = null;
29
-
30
- try {
31
- const { createSubscriberContextAPI, subscribers } = config;
32
-
33
- if (!(request.method in subscribers)) {
34
- const message = `no corresponding subscriber found for '${request.method}'`;
35
- span.setStatus({
36
- code: opentelemetry.SpanStatusCode.ERROR,
37
- message: message,
38
- });
39
- return createJSONRPCErrorResponse(
40
- request.id,
41
- JSONRPCErrorCode.MethodNotFound,
42
- message
43
- );
44
- }
45
-
46
- // The ctx argument passed into the subscriber function.
47
- const ctx = createSubscriberContextAPI({
48
- meta: request.meta,
49
- });
50
-
51
- db = createDatabaseClient({
52
- connString: request.meta?.secrets?.KEEL_DB_CONN,
53
- });
54
- const subscriberFunction = subscribers[request.method];
55
- const actionType = PROTO_ACTION_TYPES.SUBSCRIBER;
56
-
57
- const functionConfig = subscriberFunction?.config ?? {};
58
-
59
- await tryExecuteSubscriber(
60
- { request, db, actionType, functionConfig },
61
- async () => {
62
- // parse request params to convert objects into rich field types (e.g. InlineFile)
63
- const inputs = parseInputs(request.params);
64
-
65
- // Return the subscriber function to the containing tryExecuteSubscriber block
66
- return subscriberFunction(ctx, inputs);
67
- }
68
- );
69
-
70
- return createJSONRPCSuccessResponse(request.id, null);
71
- } catch (e) {
72
- if (e instanceof Error) {
73
- span.recordException(e);
74
- span.setStatus({
75
- code: opentelemetry.SpanStatusCode.ERROR,
76
- message: e.message,
77
- });
78
- return errorToJSONRPCResponse(request, e);
79
- }
80
-
81
- const message = JSON.stringify(e);
82
-
83
- span.setStatus({
84
- code: opentelemetry.SpanStatusCode.ERROR,
85
- message: message,
86
- });
87
-
88
- return createJSONRPCErrorResponse(
89
- request.id,
90
- RuntimeErrors.UnknownError,
91
- message
92
- );
93
- } finally {
94
- if (db) {
95
- await db.destroy();
96
- }
97
- }
98
- });
99
- });
100
- }
101
-
102
- module.exports = {
103
- handleSubscriber,
104
- RuntimeErrors,
105
- };