@teamkeel/functions-runtime 0.366.0-auditident7 → 0.366.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 +1 -1
- package/src/ModelAPI.js +27 -67
- package/src/auditing.js +5 -14
- package/src/database.js +1 -1
- package/src/handleRequest.test.js +0 -112
package/package.json
CHANGED
package/src/ModelAPI.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
const { useDatabase } = require("./database");
|
|
2
|
-
const { getAuditContext } = require("./auditing");
|
|
3
2
|
const { QueryBuilder } = require("./QueryBuilder");
|
|
4
3
|
const { QueryContext } = require("./QueryContext");
|
|
5
4
|
const { applyWhereConditions } = require("./applyWhereConditions");
|
|
6
5
|
const { applyJoins } = require("./applyJoins");
|
|
7
|
-
const { sql } = require("kysely");
|
|
8
6
|
|
|
9
7
|
const {
|
|
10
8
|
applyLimit,
|
|
@@ -50,52 +48,12 @@ class ModelAPI {
|
|
|
50
48
|
this._modelName = upperCamelCase(this._tableName);
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
// execute sets the audit context in the database and then runs individual
|
|
54
|
-
// database statements within a transaction if one hasn't been opened, which
|
|
55
|
-
// is necessary because the audit parameters will only be available within transactions.
|
|
56
|
-
async #execute(fn) {
|
|
57
|
-
const db = useDatabase();
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
if (db.isTransaction) {
|
|
61
|
-
await this.#setAuditParameters(db);
|
|
62
|
-
return await fn(db);
|
|
63
|
-
} else {
|
|
64
|
-
return await db.transaction().execute(async (transaction) => {
|
|
65
|
-
await this.#setAuditParameters(transaction);
|
|
66
|
-
return fn(transaction);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
} catch (e) {
|
|
70
|
-
throw new DatabaseError(e);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// setAuditParameters sets audit context data to configuration parameters
|
|
75
|
-
// in the database so that they can be read by the triggered auditing function.
|
|
76
|
-
async #setAuditParameters(transaction) {
|
|
77
|
-
const audit = getAuditContext();
|
|
78
|
-
const statements = [];
|
|
79
|
-
|
|
80
|
-
if (audit.identityId) {
|
|
81
|
-
statements.push(`CALL set_identity_id('${audit.identityId}');`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (audit.traceId) {
|
|
85
|
-
statements.push(`CALL set_trace_id('${audit.traceId}');`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (statements.length > 0) {
|
|
89
|
-
const stmt = statements.join("");
|
|
90
|
-
await sql.raw(stmt).execute(transaction);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
51
|
async create(values) {
|
|
95
52
|
const name = tracing.spanNameForModelAPI(this._modelName, "create");
|
|
53
|
+
const db = useDatabase();
|
|
96
54
|
|
|
97
55
|
return tracing.withSpan(name, async (span) => {
|
|
98
|
-
|
|
56
|
+
try {
|
|
99
57
|
const query = db
|
|
100
58
|
.insertInto(this._tableName)
|
|
101
59
|
.values(
|
|
@@ -107,8 +65,11 @@ class ModelAPI {
|
|
|
107
65
|
|
|
108
66
|
span.setAttribute("sql", query.compile().sql);
|
|
109
67
|
const row = await query.executeTakeFirstOrThrow();
|
|
68
|
+
|
|
110
69
|
return camelCaseObject(row);
|
|
111
|
-
})
|
|
70
|
+
} catch (e) {
|
|
71
|
+
throw new DatabaseError(e);
|
|
72
|
+
}
|
|
112
73
|
});
|
|
113
74
|
}
|
|
114
75
|
|
|
@@ -195,49 +156,48 @@ class ModelAPI {
|
|
|
195
156
|
|
|
196
157
|
async update(where, values) {
|
|
197
158
|
const name = tracing.spanNameForModelAPI(this._modelName, "update");
|
|
159
|
+
const db = useDatabase();
|
|
198
160
|
|
|
199
161
|
return tracing.withSpan(name, async (span) => {
|
|
200
|
-
|
|
201
|
-
let builder = db.updateTable(this._tableName).returningAll();
|
|
162
|
+
let builder = db.updateTable(this._tableName).returningAll();
|
|
202
163
|
|
|
203
|
-
|
|
164
|
+
builder = builder.set(snakeCaseObject(values));
|
|
204
165
|
|
|
205
|
-
|
|
206
|
-
[this._tableName],
|
|
207
|
-
this._tableConfigMap
|
|
208
|
-
);
|
|
166
|
+
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
209
167
|
|
|
210
|
-
|
|
211
|
-
|
|
168
|
+
// TODO: support joins for update
|
|
169
|
+
builder = applyWhereConditions(context, builder, where);
|
|
212
170
|
|
|
213
|
-
|
|
171
|
+
span.setAttribute("sql", builder.compile().sql);
|
|
214
172
|
|
|
173
|
+
try {
|
|
215
174
|
const row = await builder.executeTakeFirstOrThrow();
|
|
216
175
|
return camelCaseObject(row);
|
|
217
|
-
})
|
|
176
|
+
} catch (e) {
|
|
177
|
+
throw new DatabaseError(e);
|
|
178
|
+
}
|
|
218
179
|
});
|
|
219
180
|
}
|
|
220
181
|
|
|
221
182
|
async delete(where) {
|
|
222
183
|
const name = tracing.spanNameForModelAPI(this._modelName, "delete");
|
|
184
|
+
const db = useDatabase();
|
|
223
185
|
|
|
224
186
|
return tracing.withSpan(name, async (span) => {
|
|
225
|
-
|
|
226
|
-
let builder = db.deleteFrom(this._tableName).returning(["id"]);
|
|
187
|
+
let builder = db.deleteFrom(this._tableName).returning(["id"]);
|
|
227
188
|
|
|
228
|
-
|
|
229
|
-
[this._tableName],
|
|
230
|
-
this._tableConfigMap
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
// TODO: support joins for delete
|
|
234
|
-
builder = applyWhereConditions(context, builder, where);
|
|
189
|
+
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
235
190
|
|
|
236
|
-
|
|
191
|
+
// TODO: support joins for delete
|
|
192
|
+
builder = applyWhereConditions(context, builder, where);
|
|
237
193
|
|
|
194
|
+
span.setAttribute("sql", builder.compile().sql);
|
|
195
|
+
try {
|
|
238
196
|
const row = await builder.executeTakeFirstOrThrow();
|
|
239
197
|
return row.id;
|
|
240
|
-
})
|
|
198
|
+
} catch (e) {
|
|
199
|
+
throw new DatabaseError(e);
|
|
200
|
+
}
|
|
241
201
|
});
|
|
242
202
|
}
|
|
243
203
|
|
package/src/auditing.js
CHANGED
|
@@ -44,14 +44,6 @@ class AuditContextPlugin {
|
|
|
44
44
|
this.traceIdAlias = "__keel_trace_id";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
#setIdentityClause(value) {
|
|
48
|
-
return `set_identity_id('${value}')`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#setTraceIdClause(value) {
|
|
52
|
-
return `set_trace_id('${value}')`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
47
|
// Appends set_identity_id() and set_trace_id() function calls to the returning statement
|
|
56
48
|
// of INSERT, UPDATE and DELETE operations.
|
|
57
49
|
transformQuery(args) {
|
|
@@ -59,10 +51,13 @@ class AuditContextPlugin {
|
|
|
59
51
|
case "InsertQueryNode":
|
|
60
52
|
case "UpdateQueryNode":
|
|
61
53
|
case "DeleteQueryNode":
|
|
54
|
+
// Represents a RETURNING clause in a SQL statement.
|
|
62
55
|
const returning = {
|
|
63
56
|
kind: "ReturningNode",
|
|
64
57
|
selections: [],
|
|
65
58
|
};
|
|
59
|
+
|
|
60
|
+
// If the query already has a selection, then append it.
|
|
66
61
|
if (args.node.returning) {
|
|
67
62
|
returning.selections.push(...args.node.returning.selections);
|
|
68
63
|
}
|
|
@@ -71,10 +66,7 @@ class AuditContextPlugin {
|
|
|
71
66
|
const audit = getAuditContext();
|
|
72
67
|
|
|
73
68
|
if (audit.identityId) {
|
|
74
|
-
const rawNode = sql
|
|
75
|
-
.raw(
|
|
76
|
-
this.#setIdentityClause(audit.identityId, this.identityIdAlias)
|
|
77
|
-
)
|
|
69
|
+
const rawNode = sql`set_identity_id(${audit.identityId})`
|
|
78
70
|
.as(this.identityIdAlias)
|
|
79
71
|
.toOperationNode();
|
|
80
72
|
|
|
@@ -82,8 +74,7 @@ class AuditContextPlugin {
|
|
|
82
74
|
}
|
|
83
75
|
|
|
84
76
|
if (audit.traceId) {
|
|
85
|
-
const rawNode = sql
|
|
86
|
-
.raw(this.#setTraceIdClause(audit.traceId))
|
|
77
|
+
const rawNode = sql`set_trace_id(${audit.traceId})`
|
|
87
78
|
.as(this.traceIdAlias)
|
|
88
79
|
.toOperationNode();
|
|
89
80
|
|
package/src/database.js
CHANGED
|
@@ -282,10 +282,8 @@ describe("ModelAPI error handling", () => {
|
|
|
282
282
|
},
|
|
283
283
|
});
|
|
284
284
|
});
|
|
285
|
-
|
|
286
285
|
test("when there is a uniqueness constraint error", async () => {
|
|
287
286
|
await sql`
|
|
288
|
-
|
|
289
287
|
INSERT INTO post (id, title, author_id) values(${
|
|
290
288
|
KSUID.randomSync().string
|
|
291
289
|
}, 'hello', 'adam')
|
|
@@ -358,113 +356,3 @@ describe("ModelAPI error handling", () => {
|
|
|
358
356
|
});
|
|
359
357
|
});
|
|
360
358
|
});
|
|
361
|
-
|
|
362
|
-
// The following tests assert on the various
|
|
363
|
-
// jsonrpc responses that *should* happen when a user
|
|
364
|
-
// writes a custom function that inadvertently causes a pg constraint error to occur inside of our ModelAPI class instance.
|
|
365
|
-
describe("ModelAPI error handling", () => {
|
|
366
|
-
let functionConfig;
|
|
367
|
-
let db;
|
|
368
|
-
|
|
369
|
-
beforeEach(async () => {
|
|
370
|
-
process.env.KEEL_DB_CONN_TYPE = "pg";
|
|
371
|
-
process.env.KEEL_DB_CONN = `postgresql://postgres:postgres@localhost:5432/functions-runtime`;
|
|
372
|
-
|
|
373
|
-
db = useDatabase();
|
|
374
|
-
|
|
375
|
-
await sql`
|
|
376
|
-
DROP TABLE IF EXISTS post;
|
|
377
|
-
DROP TABLE IF EXISTS author;
|
|
378
|
-
|
|
379
|
-
CREATE TABLE author(
|
|
380
|
-
"id" text PRIMARY KEY,
|
|
381
|
-
"name" text NOT NULL
|
|
382
|
-
);
|
|
383
|
-
|
|
384
|
-
CREATE TABLE post(
|
|
385
|
-
"id" text PRIMARY KEY,
|
|
386
|
-
"title" text NOT NULL UNIQUE,
|
|
387
|
-
"author_id" text NOT NULL REFERENCES author(id)
|
|
388
|
-
);
|
|
389
|
-
`.execute(db);
|
|
390
|
-
|
|
391
|
-
await sql`
|
|
392
|
-
INSERT INTO author (id, name) VALUES ('adam', 'adam bull')
|
|
393
|
-
`.execute(db);
|
|
394
|
-
|
|
395
|
-
const models = {
|
|
396
|
-
post: new ModelAPI("post", undefined, {
|
|
397
|
-
post: {
|
|
398
|
-
author: {
|
|
399
|
-
relationshipType: "belongsTo",
|
|
400
|
-
foreignKey: "author_id",
|
|
401
|
-
referencesTable: "person",
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
}),
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
functionConfig = {
|
|
408
|
-
permissionFns: {},
|
|
409
|
-
actionTypes: {
|
|
410
|
-
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
411
|
-
deletePost: PROTO_ACTION_TYPES.DELETE,
|
|
412
|
-
},
|
|
413
|
-
functions: {
|
|
414
|
-
createPost: async (ctx, inputs) => {
|
|
415
|
-
new Permissions().allow();
|
|
416
|
-
|
|
417
|
-
const post = await models.post.create({
|
|
418
|
-
id: KSUID.randomSync().string,
|
|
419
|
-
...inputs,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
return post;
|
|
423
|
-
},
|
|
424
|
-
deletePost: async (ctx, inputs) => {
|
|
425
|
-
new Permissions().allow();
|
|
426
|
-
|
|
427
|
-
const deleted = await models.post.delete(inputs);
|
|
428
|
-
|
|
429
|
-
return deleted;
|
|
430
|
-
},
|
|
431
|
-
},
|
|
432
|
-
createContextAPI: () => ({}),
|
|
433
|
-
};
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test("when kysely returns a no result error", async () => {
|
|
437
|
-
// a kysely NoResultError is thrown when attempting to delete/update a non existent record.
|
|
438
|
-
const rpcReq = createJSONRPCRequest("123", "deletePost", {
|
|
439
|
-
id: "non-existent-id",
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
expect(await handleRequest(rpcReq, functionConfig)).toEqual({
|
|
443
|
-
id: "123",
|
|
444
|
-
jsonrpc: "2.0",
|
|
445
|
-
error: {
|
|
446
|
-
code: RuntimeErrors.RecordNotFoundError,
|
|
447
|
-
message: "no result",
|
|
448
|
-
},
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
test("when there is a not null constraint error", async () => {
|
|
453
|
-
const rpcReq = createJSONRPCRequest("123", "createPost", { title: null });
|
|
454
|
-
|
|
455
|
-
expect(await handleRequest(rpcReq, functionConfig)).toEqual({
|
|
456
|
-
id: "123",
|
|
457
|
-
jsonrpc: "2.0",
|
|
458
|
-
error: {
|
|
459
|
-
code: RuntimeErrors.NotNullConstraintError,
|
|
460
|
-
message: 'null value in column "title" violates not-null constraint',
|
|
461
|
-
data: {
|
|
462
|
-
code: "23502",
|
|
463
|
-
column: "title",
|
|
464
|
-
detail: expect.stringContaining("Failing row contains"),
|
|
465
|
-
table: "post",
|
|
466
|
-
},
|
|
467
|
-
},
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
});
|