@teamkeel/functions-runtime 0.330.3 → 0.332.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamkeel/functions-runtime",
3
- "version": "0.330.3",
3
+ "version": "0.332.0",
4
4
  "description": "Internal package used by @teamkeel/sdk",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -21,9 +21,11 @@
21
21
  "dependencies": {
22
22
  "@opentelemetry/api": "^1.4.1",
23
23
  "@opentelemetry/exporter-trace-otlp-proto": "^0.38.0",
24
+ "@opentelemetry/instrumentation": "^0.40.0",
24
25
  "@opentelemetry/resources": "^1.12.0",
25
26
  "@opentelemetry/sdk-trace-base": "^1.12.0",
26
27
  "@opentelemetry/sdk-trace-node": "^1.12.0",
28
+ "@prisma/instrumentation": "^4.15.0",
27
29
  "change-case": "^4.1.2",
28
30
  "json-rpc-2.0": "^1.4.1",
29
31
  "ksuid": "^3.0.0",
package/pnpm-lock.yaml CHANGED
@@ -3,9 +3,11 @@ lockfileVersion: 5.4
3
3
  specifiers:
4
4
  "@opentelemetry/api": ^1.4.1
5
5
  "@opentelemetry/exporter-trace-otlp-proto": ^0.38.0
6
+ "@opentelemetry/instrumentation": ^0.40.0
6
7
  "@opentelemetry/resources": ^1.12.0
7
8
  "@opentelemetry/sdk-trace-base": ^1.12.0
8
9
  "@opentelemetry/sdk-trace-node": ^1.12.0
10
+ "@prisma/instrumentation": ^4.15.0
9
11
  change-case: ^4.1.2
10
12
  json-rpc-2.0: ^1.4.1
11
13
  ksuid: ^3.0.0
@@ -17,9 +19,11 @@ specifiers:
17
19
  dependencies:
18
20
  "@opentelemetry/api": 1.4.1
19
21
  "@opentelemetry/exporter-trace-otlp-proto": 0.38.0_@opentelemetry+api@1.4.1
22
+ "@opentelemetry/instrumentation": 0.40.0_@opentelemetry+api@1.4.1
20
23
  "@opentelemetry/resources": 1.12.0_@opentelemetry+api@1.4.1
21
24
  "@opentelemetry/sdk-trace-base": 1.12.0_@opentelemetry+api@1.4.1
22
25
  "@opentelemetry/sdk-trace-node": 1.12.0_@opentelemetry+api@1.4.1
26
+ "@prisma/instrumentation": 4.15.0
23
27
  change-case: 4.1.2
24
28
  json-rpc-2.0: 1.4.2
25
29
  ksuid: 3.0.0
@@ -328,6 +332,19 @@ packages:
328
332
  "@opentelemetry/semantic-conventions": 1.12.0
329
333
  dev: false
330
334
 
335
+ /@opentelemetry/core/1.13.0_@opentelemetry+api@1.4.1:
336
+ resolution:
337
+ {
338
+ integrity: sha512-2dBX3Sj99H96uwJKvc2w9NOiNgbvAO6mOFJFramNkKfS9O4Um+VWgpnlAazoYjT6kUJ1MP70KQ5ngD4ed+4NUw==,
339
+ }
340
+ engines: { node: ">=14" }
341
+ peerDependencies:
342
+ "@opentelemetry/api": ">=1.0.0 <1.5.0"
343
+ dependencies:
344
+ "@opentelemetry/api": 1.4.1
345
+ "@opentelemetry/semantic-conventions": 1.13.0
346
+ dev: false
347
+
331
348
  /@opentelemetry/exporter-trace-otlp-proto/0.38.0_@opentelemetry+api@1.4.1:
332
349
  resolution:
333
350
  {
@@ -346,6 +363,42 @@ packages:
346
363
  "@opentelemetry/sdk-trace-base": 1.12.0_@opentelemetry+api@1.4.1
347
364
  dev: false
348
365
 
366
+ /@opentelemetry/instrumentation/0.39.1_@opentelemetry+api@1.4.1:
367
+ resolution:
368
+ {
369
+ integrity: sha512-s7/9tPmM0l5KCd07VQizC4AO2/5UJdkXq5gMSHPdCeiMKSeBEdyDyQX7A+Cq+RYZM452qzFmrJ4ut628J5bnSg==,
370
+ }
371
+ engines: { node: ">=14" }
372
+ peerDependencies:
373
+ "@opentelemetry/api": ^1.3.0
374
+ dependencies:
375
+ "@opentelemetry/api": 1.4.1
376
+ require-in-the-middle: 7.1.1
377
+ semver: 7.5.0
378
+ shimmer: 1.2.1
379
+ transitivePeerDependencies:
380
+ - supports-color
381
+ dev: false
382
+
383
+ /@opentelemetry/instrumentation/0.40.0_@opentelemetry+api@1.4.1:
384
+ resolution:
385
+ {
386
+ integrity: sha512-23TzBKPflUS1uEq5SXymnQKQDSda35KvHjnvxdcDQGE+wg6hwDHgScUCWiBmZW4sxAaPcANfs+Wc9B7yDuyT6Q==,
387
+ }
388
+ engines: { node: ">=14" }
389
+ peerDependencies:
390
+ "@opentelemetry/api": ^1.3.0
391
+ dependencies:
392
+ "@opentelemetry/api": 1.4.1
393
+ "@types/shimmer": 1.0.2
394
+ import-in-the-middle: 1.3.5
395
+ require-in-the-middle: 7.1.1
396
+ semver: 7.5.0
397
+ shimmer: 1.2.1
398
+ transitivePeerDependencies:
399
+ - supports-color
400
+ dev: false
401
+
349
402
  /@opentelemetry/otlp-exporter-base/0.38.0_@opentelemetry+api@1.4.1:
350
403
  resolution:
351
404
  {
@@ -430,6 +483,20 @@ packages:
430
483
  "@opentelemetry/semantic-conventions": 1.12.0
431
484
  dev: false
432
485
 
486
+ /@opentelemetry/resources/1.13.0_@opentelemetry+api@1.4.1:
487
+ resolution:
488
+ {
489
+ integrity: sha512-euqjOkiN6xhjE//0vQYGvbStxoD/WWQRhDiO0OTLlnLBO9Yw2Gd/VoSx2H+svsebjzYk5OxLuREBmcdw6rbUNg==,
490
+ }
491
+ engines: { node: ">=14" }
492
+ peerDependencies:
493
+ "@opentelemetry/api": ">=1.0.0 <1.5.0"
494
+ dependencies:
495
+ "@opentelemetry/api": 1.4.1
496
+ "@opentelemetry/core": 1.13.0_@opentelemetry+api@1.4.1
497
+ "@opentelemetry/semantic-conventions": 1.13.0
498
+ dev: false
499
+
433
500
  /@opentelemetry/sdk-metrics/1.12.0_@opentelemetry+api@1.4.1:
434
501
  resolution:
435
502
  {
@@ -460,6 +527,21 @@ packages:
460
527
  "@opentelemetry/semantic-conventions": 1.12.0
461
528
  dev: false
462
529
 
530
+ /@opentelemetry/sdk-trace-base/1.13.0_@opentelemetry+api@1.4.1:
531
+ resolution:
532
+ {
533
+ integrity: sha512-moTiQtc0uPR1hQLt6gLDJH9IIkeBhgRb71OKjNHZPE1VF45fHtD6nBDi5J/DkTHTwYP5X3kBJLa3xN7ub6J4eg==,
534
+ }
535
+ engines: { node: ">=14" }
536
+ peerDependencies:
537
+ "@opentelemetry/api": ">=1.0.0 <1.5.0"
538
+ dependencies:
539
+ "@opentelemetry/api": 1.4.1
540
+ "@opentelemetry/core": 1.13.0_@opentelemetry+api@1.4.1
541
+ "@opentelemetry/resources": 1.13.0_@opentelemetry+api@1.4.1
542
+ "@opentelemetry/semantic-conventions": 1.13.0
543
+ dev: false
544
+
463
545
  /@opentelemetry/sdk-trace-node/1.12.0_@opentelemetry+api@1.4.1:
464
546
  resolution:
465
547
  {
@@ -486,6 +568,27 @@ packages:
486
568
  engines: { node: ">=14" }
487
569
  dev: false
488
570
 
571
+ /@opentelemetry/semantic-conventions/1.13.0:
572
+ resolution:
573
+ {
574
+ integrity: sha512-LMGqfSZkaMQXqewO0o1wvWr/2fQdCh4a3Sqlxka/UsJCe0cfLulh6x2aqnKLnsrSGiCq5rSCwvINd152i0nCqw==,
575
+ }
576
+ engines: { node: ">=14" }
577
+ dev: false
578
+
579
+ /@prisma/instrumentation/4.15.0:
580
+ resolution:
581
+ {
582
+ integrity: sha512-sTqYGci90Oxfs1HRAonvQ3eVmFNqzBG4PJHekkytZf9oAfk9ADdLFNI0/xzgXW3K9FUBfhpBDfJj/6G2CWxPTA==,
583
+ }
584
+ dependencies:
585
+ "@opentelemetry/api": 1.4.1
586
+ "@opentelemetry/instrumentation": 0.39.1_@opentelemetry+api@1.4.1
587
+ "@opentelemetry/sdk-trace-base": 1.13.0_@opentelemetry+api@1.4.1
588
+ transitivePeerDependencies:
589
+ - supports-color
590
+ dev: false
591
+
489
592
  /@protobufjs/aspromise/1.1.2:
490
593
  resolution:
491
594
  {
@@ -581,6 +684,13 @@ packages:
581
684
  integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==,
582
685
  }
583
686
 
687
+ /@types/shimmer/1.0.2:
688
+ resolution:
689
+ {
690
+ integrity: sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==,
691
+ }
692
+ dev: false
693
+
584
694
  /acorn-walk/8.2.0:
585
695
  resolution:
586
696
  {
@@ -723,7 +833,6 @@ packages:
723
833
  optional: true
724
834
  dependencies:
725
835
  ms: 2.1.2
726
- dev: true
727
836
 
728
837
  /deep-eql/4.1.3:
729
838
  resolution:
@@ -794,7 +903,6 @@ packages:
794
903
  {
795
904
  integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==,
796
905
  }
797
- dev: true
798
906
 
799
907
  /get-func-name/2.0.0:
800
908
  resolution:
@@ -811,7 +919,6 @@ packages:
811
919
  engines: { node: ">= 0.4.0" }
812
920
  dependencies:
813
921
  function-bind: 1.1.1
814
- dev: true
815
922
 
816
923
  /header-case/2.0.4:
817
924
  resolution:
@@ -823,6 +930,15 @@ packages:
823
930
  tslib: 2.4.1
824
931
  dev: false
825
932
 
933
+ /import-in-the-middle/1.3.5:
934
+ resolution:
935
+ {
936
+ integrity: sha512-yzHlBqi1EBFrkieAnSt8eTgO5oLSl+YJ7qaOpUH/PMqQOMZoQ/RmDlwnTLQrwYto+gHYjRG+i/IbsB1eDx32NQ==,
937
+ }
938
+ dependencies:
939
+ module-details-from-path: 1.0.3
940
+ dev: false
941
+
826
942
  /is-core-module/2.11.0:
827
943
  resolution:
828
944
  {
@@ -830,7 +946,6 @@ packages:
830
946
  }
831
947
  dependencies:
832
948
  has: 1.0.3
833
- dev: true
834
949
 
835
950
  /json-rpc-2.0/1.4.2:
836
951
  resolution:
@@ -911,6 +1026,7 @@ packages:
911
1026
  engines: { node: ">=10" }
912
1027
  dependencies:
913
1028
  yallist: 4.0.0
1029
+ dev: false
914
1030
 
915
1031
  /mlly/1.1.0:
916
1032
  resolution:
@@ -924,12 +1040,18 @@ packages:
924
1040
  ufo: 1.0.1
925
1041
  dev: true
926
1042
 
1043
+ /module-details-from-path/1.0.3:
1044
+ resolution:
1045
+ {
1046
+ integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==,
1047
+ }
1048
+ dev: false
1049
+
927
1050
  /ms/2.1.2:
928
1051
  resolution:
929
1052
  {
930
1053
  integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==,
931
1054
  }
932
- dev: true
933
1055
 
934
1056
  /nanoid/3.3.4:
935
1057
  resolution:
@@ -992,7 +1114,6 @@ packages:
992
1114
  {
993
1115
  integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==,
994
1116
  }
995
- dev: true
996
1117
 
997
1118
  /pathe/0.2.0:
998
1119
  resolution:
@@ -1187,6 +1308,20 @@ packages:
1187
1308
  long: 5.2.3
1188
1309
  dev: false
1189
1310
 
1311
+ /require-in-the-middle/7.1.1:
1312
+ resolution:
1313
+ {
1314
+ integrity: sha512-OScOjQjrrjhAdFpQmnkE/qbIBGCRFhQB/YaJhcC3CPOlmhe7llnW46Ac1J5+EjcNXOTnDdpF96Erw/yedsGksQ==,
1315
+ }
1316
+ engines: { node: ">=8.6.0" }
1317
+ dependencies:
1318
+ debug: 4.3.4
1319
+ module-details-from-path: 1.0.3
1320
+ resolve: 1.22.1
1321
+ transitivePeerDependencies:
1322
+ - supports-color
1323
+ dev: false
1324
+
1190
1325
  /resolve/1.22.1:
1191
1326
  resolution:
1192
1327
  {
@@ -1197,7 +1332,6 @@ packages:
1197
1332
  is-core-module: 2.11.0
1198
1333
  path-parse: 1.0.7
1199
1334
  supports-preserve-symlinks-flag: 1.0.0
1200
- dev: true
1201
1335
 
1202
1336
  /rollup/3.10.0:
1203
1337
  resolution:
@@ -1232,6 +1366,13 @@ packages:
1232
1366
  upper-case-first: 2.0.2
1233
1367
  dev: false
1234
1368
 
1369
+ /shimmer/1.2.1:
1370
+ resolution:
1371
+ {
1372
+ integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==,
1373
+ }
1374
+ dev: false
1375
+
1235
1376
  /siginfo/2.0.0:
1236
1377
  resolution:
1237
1378
  {
@@ -1305,7 +1446,6 @@ packages:
1305
1446
  integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==,
1306
1447
  }
1307
1448
  engines: { node: ">= 0.4" }
1308
- dev: true
1309
1449
 
1310
1450
  /tinybench/2.3.1:
1311
1451
  resolution:
@@ -1510,3 +1650,4 @@ packages:
1510
1650
  {
1511
1651
  integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==,
1512
1652
  }
1653
+ dev: false
package/src/database.js CHANGED
@@ -1,6 +1,27 @@
1
1
  const { Kysely, PostgresDialect } = require("kysely");
2
2
  const { AsyncLocalStorage } = require("async_hooks");
3
3
  const pg = require("pg");
4
+ const { PROTO_ACTION_TYPES } = require("./consts");
5
+
6
+ // withTransaction wraps the containing code with a transaction
7
+ // and sets the transaction in the AsyncLocalStorage so consumers further
8
+ // down the hierarchy can access the current transaction.
9
+ // For read type operations such as list & get, no transaction is used
10
+ async function withTransaction(db, actionType, cb) {
11
+ switch (actionType) {
12
+ case PROTO_ACTION_TYPES.GET:
13
+ case PROTO_ACTION_TYPES.LIST:
14
+ return dbInstance.run(db, async () => {
15
+ return cb({ transaction: db });
16
+ });
17
+ default:
18
+ return db.transaction().execute(async (transaction) => {
19
+ return dbInstance.run(transaction, async () => {
20
+ return cb({ transaction });
21
+ });
22
+ });
23
+ }
24
+ }
4
25
 
5
26
  function mustEnv(key) {
6
27
  const v = process.env[key];
@@ -47,5 +68,5 @@ function getDatabase() {
47
68
  return db;
48
69
  }
49
70
 
50
- module.exports.dbInstance = dbInstance;
51
71
  module.exports.getDatabase = getDatabase;
72
+ module.exports.withTransaction = withTransaction;
@@ -3,18 +3,11 @@ const {
3
3
  createJSONRPCSuccessResponse,
4
4
  JSONRPCErrorCode,
5
5
  } = require("json-rpc-2.0");
6
- const { getDatabase, dbInstance } = require("./database");
7
- const {
8
- PERMISSION_STATE,
9
- Permissions,
10
- PermissionError,
11
- checkBuiltInPermissions,
12
- permissionsApiInstance,
13
- } = require("./permissions");
14
- const { PROTO_ACTION_TYPES } = require("./consts");
6
+ const { getDatabase } = require("./database");
7
+ const { tryExecuteFunction } = require("./tryExecuteFunction");
15
8
  const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
16
9
  const opentelemetry = require("@opentelemetry/api");
17
- const { getTracer, withSpan } = require("./tracing");
10
+ const { withSpan } = require("./tracing");
18
11
 
19
12
  // Generic handler function that is agnostic to runtime environment (local or lambda)
20
13
  // to execute a custom function based on the contents of a jsonrpc-2.0 payload object.
@@ -56,80 +49,29 @@ async function handleRequest(request, config) {
56
49
  meta: request.meta,
57
50
  });
58
51
 
52
+ // The Go runtime does *some* permissions checks up front before the request reaches
53
+ // this method, so we pass in a permissionState object on the request.meta object that
54
+ // indicates whether a call to a custom function has already been authorised
59
55
  const permitted =
60
56
  request.meta && request.meta.permissionState.status === "granted"
61
57
  ? true
62
58
  : null;
63
59
 
64
60
  const db = getDatabase();
65
- const permissions = new Permissions();
66
-
67
- const result = await permissionsApiInstance.run(
68
- { permitted: permitted },
69
- () => {
70
- // We want to wrap the execution of the custom function in a transaction so that any call the user makes
71
- // to any of the model apis we provide to the custom function is processed in a transaction.
72
- // This is useful for permissions where we want to only proceed with database writes if all permission rules
73
- // have been validated.
74
-
75
- return db.transaction().execute(async (transaction) => {
76
- return dbInstance.run(transaction, async () => {
77
- // Call the user's custom function!
78
- const customFunction = functions[request.method];
79
- const fnResult = await customFunction(ctx, request.params);
80
-
81
- // api.permissions maintains an internal state of whether the current operation has been *explicitly* permitted/denied by the user in the course of their custom function, or if execution has already been permitted by a role based permission (evaluated in the main runtime).
82
- // we need to check that the final state is permitted or unpermitted. if it's not, then it means that the user has taken no explicit action to permit/deny
83
- // and therefore we default to checking the permissions defined in the schema automatically.
84
- switch (permissions.getState()) {
85
- case PERMISSION_STATE.PERMITTED:
86
- return fnResult;
87
- case PERMISSION_STATE.UNPERMITTED:
88
- throw new PermissionError(
89
- `Not permitted to access ${request.method}`
90
- );
91
- default:
92
- // unknown state, proceed with checking against the built in permissions in the schema
93
- const relevantPermissions = permissionFns[request.method];
94
- const actionType = actionTypes[request.method];
95
-
96
- const peakInsideTransaction =
97
- actionType === PROTO_ACTION_TYPES.CREATE;
98
-
99
- let rowsForPermissions = [];
100
- switch (actionType) {
101
- case PROTO_ACTION_TYPES.LIST:
102
- rowsForPermissions = fnResult;
103
- break;
104
- case PROTO_ACTION_TYPES.DELETE:
105
- rowsForPermissions = [{ id: fnResult }];
106
- break;
107
- default:
108
- rowsForPermissions = [fnResult];
109
- break;
110
- }
111
-
112
- // check will throw a PermissionError if a permission rule is invalid
113
- await checkBuiltInPermissions({
114
- rows: rowsForPermissions,
115
- permissionFns: relevantPermissions,
116
- // it is important that we pass db here as db represents the connection to the database
117
- // *outside* of the current transaction. Given that any changes inside of a transaction
118
- // are opaque to the outside, we can utilize this when running permission rules and then deciding to
119
- // rollback any changes if they do not pass. However, for creates we need to be able to 'peak' inside the transaction to read the created record, as this won't exist outside of the transaction.
120
- db: peakInsideTransaction ? transaction : db,
121
- ctx,
122
- functionName: request.method,
123
- });
124
-
125
- // If the built in permission check above doesn't throw, then it means that the request is permitted and we can continue returning the return value from the custom function out of the transaction
126
- return fnResult;
127
- }
128
- });
129
- });
61
+ const customFunction = functions[request.method];
62
+
63
+ const result = await tryExecuteFunction(
64
+ { request, ctx, permitted, db, permissionFns, actionTypes },
65
+ async () => {
66
+ // Return the custom function to the containing tryExecuteFunction block
67
+ // Once the custom function is called, tryExecuteFunction will check the schema's permission rules to see if it can continue committing
68
+ // the transaction to the db. If a permission rule is violated, any changes made inside the transaction are rolled back.
69
+ return customFunction(ctx, request.params);
130
70
  }
131
71
  );
132
72
 
73
+ // Sometimes a custom function may be coded in such a way that nothing is returned from it.
74
+ // We see this as an error so handle accordingly.
133
75
  if (result === undefined) {
134
76
  // no result returned from custom function
135
77
  return createJSONRPCErrorResponse(
@@ -232,6 +232,10 @@ describe("ModelAPI error handling", () => {
232
232
 
233
233
  functionConfig = {
234
234
  permissionFns: {},
235
+ actionTypes: {
236
+ createPost: PROTO_ACTION_TYPES.CREATE,
237
+ deletePost: PROTO_ACTION_TYPES.DELETE,
238
+ },
235
239
  functions: {
236
240
  createPost: async (ctx, inputs) => {
237
241
  new Permissions().allow();
@@ -8,6 +8,16 @@ const PERMISSION_STATE = {
8
8
  UNPERMITTED: "unpermitted",
9
9
  };
10
10
 
11
+ // withPermissions sets the initial permission state from the go runtime in the AsyncLocalStorage so consumers further down the hierarchy can read or mutate the state
12
+ // at will
13
+ const withPermissions = async (initialValue, cb) => {
14
+ const permissions = new Permissions();
15
+
16
+ return await permissionsApiInstance.run({ permitted: initialValue }, () => {
17
+ return cb({ getPermissionState: permissions.getState });
18
+ });
19
+ };
20
+
11
21
  const permissionsApiInstance = new AsyncLocalStorage();
12
22
 
13
23
  class Permissions {
@@ -61,8 +71,9 @@ const checkBuiltInPermissions = async ({
61
71
  throw new PermissionError(`Not permitted to access ${functionName}`);
62
72
  };
63
73
 
64
- module.exports.permissionsApiInstance = permissionsApiInstance;
65
74
  module.exports.checkBuiltInPermissions = checkBuiltInPermissions;
66
75
  module.exports.PermissionError = PermissionError;
67
76
  module.exports.PERMISSION_STATE = PERMISSION_STATE;
68
77
  module.exports.Permissions = Permissions;
78
+ module.exports.withPermissions = withPermissions;
79
+ module.exports.permissionsApiInstance = permissionsApiInstance;
package/src/tracing.js CHANGED
@@ -5,6 +5,8 @@ const {
5
5
  } = require("@opentelemetry/exporter-trace-otlp-proto");
6
6
  const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
7
7
  const { envDetectorSync } = require("@opentelemetry/resources");
8
+ const { registerInstrumentations } = require("@opentelemetry/instrumentation");
9
+ const { PrismaInstrumentation } = require("@prisma/instrumentation");
8
10
 
9
11
  function withSpan(name, fn) {
10
12
  return getTracer().startActiveSpan(name, async (span) => {
@@ -61,6 +63,12 @@ function init() {
61
63
  });
62
64
  const exporter = new OTLPTraceExporter();
63
65
  const processor = new BatchSpanProcessor(exporter);
66
+
67
+ registerInstrumentations({
68
+ tracerProvider: provider,
69
+ instrumentations: [new PrismaInstrumentation()],
70
+ });
71
+
64
72
  provider.addSpanProcessor(processor);
65
73
  provider.register();
66
74
  }
@@ -0,0 +1,72 @@
1
+ const { withTransaction } = require("./database");
2
+ const {
3
+ withPermissions,
4
+ PERMISSION_STATE,
5
+ PermissionError,
6
+ checkBuiltInPermissions,
7
+ } = require("./permissions");
8
+ const { PROTO_ACTION_TYPES } = require("./consts");
9
+
10
+ // tryExecuteFunction will create a new database transaction around a function call
11
+ // and handle any permissions checks. If a permission check fails, then an Error will be thrown and the catch block will be hit.
12
+ function tryExecuteFunction(
13
+ { db, permitted, permissionFns, actionTypes, request, ctx },
14
+ cb
15
+ ) {
16
+ const actionType = actionTypes[request.method];
17
+
18
+ return withPermissions(permitted, async ({ getPermissionState }) => {
19
+ return withTransaction(db, actionType, async ({ transaction }) => {
20
+ const fnResult = await cb();
21
+
22
+ // api.permissions maintains an internal state of whether the current operation has been *explicitly* permitted/denied by the user in the course of their custom function, or if execution has already been permitted by a role based permission (evaluated in the main runtime).
23
+ // we need to check that the final state is permitted or unpermitted. if it's not, then it means that the user has taken no explicit action to permit/deny
24
+ // and therefore we default to checking the permissions defined in the schema automatically.
25
+ switch (getPermissionState()) {
26
+ case PERMISSION_STATE.PERMITTED:
27
+ return fnResult;
28
+ case PERMISSION_STATE.UNPERMITTED:
29
+ throw new PermissionError(
30
+ `Not permitted to access ${request.method}`
31
+ );
32
+ default:
33
+ // unknown state, proceed with checking against the built in permissions in the schema
34
+ const relevantPermissions = permissionFns[request.method];
35
+
36
+ const peakInsideTransaction =
37
+ actionType === PROTO_ACTION_TYPES.CREATE;
38
+
39
+ let rowsForPermissions = [];
40
+ switch (actionType) {
41
+ case PROTO_ACTION_TYPES.LIST:
42
+ rowsForPermissions = fnResult;
43
+ break;
44
+ case PROTO_ACTION_TYPES.DELETE:
45
+ rowsForPermissions = [{ id: fnResult }];
46
+ break;
47
+ default:
48
+ rowsForPermissions = [fnResult];
49
+ break;
50
+ }
51
+
52
+ // check will throw a PermissionError if a permission rule is invalid
53
+ await checkBuiltInPermissions({
54
+ rows: rowsForPermissions,
55
+ permissionFns: relevantPermissions,
56
+ // it is important that we pass db here as db represents the connection to the database
57
+ // *outside* of the current transaction. Given that any changes inside of a transaction
58
+ // are opaque to the outside, we can utilize this when running permission rules and then deciding to
59
+ // rollback any changes if they do not pass. However, for creates we need to be able to 'peak' inside the transaction to read the created record, as this won't exist outside of the transaction.
60
+ db: peakInsideTransaction ? transaction : db,
61
+ ctx,
62
+ functionName: request.method,
63
+ });
64
+
65
+ // If the built in permission check above doesn't throw, then it means that the request is permitted and we can continue returning the return value from the custom function out of the transaction
66
+ return fnResult;
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ module.exports.tryExecuteFunction = tryExecuteFunction;