@teamkeel/functions-runtime 0.412.0-next.2 → 0.412.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/.env.test +2 -0
- package/compose.yaml +10 -0
- package/package.json +5 -23
- package/src/Duration.js +40 -0
- package/src/Duration.test.js +34 -0
- package/src/File.js +295 -0
- package/src/ModelAPI.js +377 -0
- package/src/ModelAPI.test.js +1428 -0
- package/src/QueryBuilder.js +184 -0
- package/src/QueryContext.js +90 -0
- package/src/RequestHeaders.js +21 -0
- package/src/TimePeriod.js +89 -0
- package/src/TimePeriod.test.js +148 -0
- package/src/applyAdditionalQueryConstraints.js +22 -0
- package/src/applyJoins.js +67 -0
- package/src/applyWhereConditions.js +124 -0
- package/src/auditing.js +110 -0
- package/src/auditing.test.js +330 -0
- package/src/camelCasePlugin.js +52 -0
- package/src/casing.js +54 -0
- package/src/casing.test.js +56 -0
- package/src/consts.js +14 -0
- package/src/database.js +244 -0
- package/src/errors.js +160 -0
- package/src/handleJob.js +110 -0
- package/src/handleJob.test.js +270 -0
- package/src/handleRequest.js +153 -0
- package/src/handleRequest.test.js +463 -0
- package/src/handleRoute.js +112 -0
- package/src/handleSubscriber.js +105 -0
- package/src/index.d.ts +317 -0
- package/src/index.js +38 -0
- package/src/parsing.js +113 -0
- package/src/parsing.test.js +140 -0
- package/src/permissions.js +77 -0
- package/src/permissions.test.js +118 -0
- package/src/tracing.js +184 -0
- package/src/tracing.test.js +147 -0
- package/src/tryExecuteFunction.js +91 -0
- package/src/tryExecuteJob.js +29 -0
- package/src/tryExecuteSubscriber.js +17 -0
- package/src/type-utils.js +18 -0
- package/vite.config.js +7 -0
- package/dist/index.d.mts +0 -340
- package/dist/index.d.ts +0 -340
- package/dist/index.js +0 -3093
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -3097
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { sql, Kysely } = require("kysely");
|
|
2
|
+
const { snakeCase } = require("./casing");
|
|
3
|
+
const { TimePeriod } = require("./TimePeriod");
|
|
4
|
+
|
|
5
|
+
const opMapping = {
|
|
6
|
+
startsWith: { op: "like", value: (v) => `${v}%` },
|
|
7
|
+
endsWith: { op: "like", value: (v) => `%${v}` },
|
|
8
|
+
contains: { op: "like", value: (v) => `%${v}%` },
|
|
9
|
+
oneOf: { op: "=", value: (v) => sql`ANY(${v})` },
|
|
10
|
+
greaterThan: { op: ">" },
|
|
11
|
+
greaterThanOrEquals: { op: ">=" },
|
|
12
|
+
lessThan: { op: "<" },
|
|
13
|
+
lessThanOrEquals: { op: "<=" },
|
|
14
|
+
before: { op: "<" },
|
|
15
|
+
onOrBefore: { op: "<=" },
|
|
16
|
+
after: { op: ">" },
|
|
17
|
+
onOrAfter: { op: ">=" },
|
|
18
|
+
equals: { op: sql`is not distinct from` },
|
|
19
|
+
notEquals: { op: sql`is distinct from` },
|
|
20
|
+
equalsRelative: {
|
|
21
|
+
op: sql`BETWEEN`,
|
|
22
|
+
value: (v) =>
|
|
23
|
+
sql`${sql.raw(
|
|
24
|
+
TimePeriod.fromExpression(v).periodStartSQL()
|
|
25
|
+
)} AND ${sql.raw(TimePeriod.fromExpression(v).periodEndSQL())}`,
|
|
26
|
+
},
|
|
27
|
+
beforeRelative: {
|
|
28
|
+
op: "<",
|
|
29
|
+
value: (v) =>
|
|
30
|
+
sql`${sql.raw(TimePeriod.fromExpression(v).periodStartSQL())}`,
|
|
31
|
+
},
|
|
32
|
+
afterRelative: {
|
|
33
|
+
op: ">=",
|
|
34
|
+
value: (v) => sql`${sql.raw(TimePeriod.fromExpression(v).periodEndSQL())}`,
|
|
35
|
+
},
|
|
36
|
+
any: {
|
|
37
|
+
isArrayQuery: true,
|
|
38
|
+
greaterThan: { op: ">" },
|
|
39
|
+
greaterThanOrEquals: { op: ">=" },
|
|
40
|
+
lessThan: { op: "<" },
|
|
41
|
+
lessThanOrEquals: { op: "<=" },
|
|
42
|
+
before: { op: "<" },
|
|
43
|
+
onOrBefore: { op: "<=" },
|
|
44
|
+
after: { op: ">" },
|
|
45
|
+
onOrAfter: { op: ">=" },
|
|
46
|
+
equals: { op: "=" },
|
|
47
|
+
notEquals: { op: "=", value: (v) => sql`NOT ${v}` },
|
|
48
|
+
},
|
|
49
|
+
all: {
|
|
50
|
+
isArrayQuery: true,
|
|
51
|
+
greaterThan: { op: ">" },
|
|
52
|
+
greaterThanOrEquals: { op: ">=" },
|
|
53
|
+
lessThan: { op: "<" },
|
|
54
|
+
lessThanOrEquals: { op: "<=" },
|
|
55
|
+
before: { op: "<" },
|
|
56
|
+
onOrBefore: { op: "<=" },
|
|
57
|
+
after: { op: ">" },
|
|
58
|
+
onOrAfter: { op: ">=" },
|
|
59
|
+
equals: { op: "=" },
|
|
60
|
+
notEquals: { op: "=", value: (v) => sql`NOT ${v}` },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Applies the given where conditions to the provided Kysely
|
|
66
|
+
* instance and returns the resulting new Kysely instance.
|
|
67
|
+
* @param {import("./QueryContext").QueryContext} context
|
|
68
|
+
* @param {import("kysely").Kysely} qb
|
|
69
|
+
* @param {Object} where
|
|
70
|
+
* @returns {import("kysely").Kysely}
|
|
71
|
+
*/
|
|
72
|
+
function applyWhereConditions(context, qb, where = {}) {
|
|
73
|
+
const conf = context.tableConfig();
|
|
74
|
+
for (const key of Object.keys(where)) {
|
|
75
|
+
const v = where[key];
|
|
76
|
+
|
|
77
|
+
// Handle nested where conditions e.g. using a join table
|
|
78
|
+
if (conf && conf[snakeCase(key)]) {
|
|
79
|
+
const rel = conf[snakeCase(key)];
|
|
80
|
+
context.withJoin(rel.referencesTable, () => {
|
|
81
|
+
qb = applyWhereConditions(context, qb, v);
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fieldName = `${context.tableAlias()}.${snakeCase(key)}`;
|
|
87
|
+
|
|
88
|
+
if (Object.prototype.toString.call(v) !== "[object Object]") {
|
|
89
|
+
qb = qb.where(fieldName, sql`is not distinct from`, sql`${v}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const op of Object.keys(v)) {
|
|
94
|
+
const mapping = opMapping[op];
|
|
95
|
+
if (!mapping) {
|
|
96
|
+
throw new Error(`invalid where condition: ${op}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (mapping.isArrayQuery) {
|
|
100
|
+
for (const arrayOp of Object.keys(v[op])) {
|
|
101
|
+
qb = qb.where(
|
|
102
|
+
mapping[arrayOp].value
|
|
103
|
+
? mapping[arrayOp].value(v[op][arrayOp])
|
|
104
|
+
: sql`${v[op][arrayOp]}`,
|
|
105
|
+
mapping[arrayOp].op,
|
|
106
|
+
sql`${sql(op)}(${sql.ref(fieldName)})`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
qb = qb.where(
|
|
111
|
+
fieldName,
|
|
112
|
+
mapping.op,
|
|
113
|
+
mapping.value ? mapping.value(v[op]) : sql`${v[op]}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return qb;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
applyWhereConditions,
|
|
124
|
+
};
|
package/src/auditing.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const { AsyncLocalStorage } = require("async_hooks");
|
|
2
|
+
const TraceParent = require("traceparent");
|
|
3
|
+
const { sql, SelectionNode } = require("kysely");
|
|
4
|
+
|
|
5
|
+
const auditContextStorage = new AsyncLocalStorage();
|
|
6
|
+
|
|
7
|
+
// withAuditContext creates the audit context from the runtime request body
|
|
8
|
+
// and sets it to in AsyncLocalStorage so that this data is available to the
|
|
9
|
+
// ModelAPI during the execution of actions, jobs and subscribers.
|
|
10
|
+
async function withAuditContext(request, cb) {
|
|
11
|
+
let audit = {};
|
|
12
|
+
|
|
13
|
+
if (request.meta?.identity) {
|
|
14
|
+
audit.identityId = request.meta.identity.id;
|
|
15
|
+
}
|
|
16
|
+
if (request.meta?.tracing?.traceparent) {
|
|
17
|
+
audit.traceId = TraceParent.fromString(
|
|
18
|
+
request.meta.tracing.traceparent
|
|
19
|
+
)?.traceId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return await auditContextStorage.run(audit, () => {
|
|
23
|
+
return cb();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// getAuditContext retrieves the audit context from AsyncLocalStorage.
|
|
28
|
+
function getAuditContext() {
|
|
29
|
+
let auditStore = auditContextStorage.getStore();
|
|
30
|
+
return {
|
|
31
|
+
identityId: auditStore?.identityId,
|
|
32
|
+
traceId: auditStore?.traceId,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// AuditContextPlugin is a Kysely plugin which ensures that the audit context data
|
|
37
|
+
// is written to Postgres configuration parameters in the same execution as a query.
|
|
38
|
+
// It does this by calling the set_identity_id() and set_trace_id() functions as a
|
|
39
|
+
// clause in the returning statement. It then subsequently drops these from the actual result.
|
|
40
|
+
// This ensures that these parameters are set when the tables' AFTER trigger function executes.
|
|
41
|
+
class AuditContextPlugin {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.identityIdAlias = "__keel_identity_id";
|
|
44
|
+
this.traceIdAlias = "__keel_trace_id";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Appends set_identity_id() and set_trace_id() function calls to the returning statement
|
|
48
|
+
// of INSERT, UPDATE and DELETE operations.
|
|
49
|
+
transformQuery(args) {
|
|
50
|
+
switch (args.node.kind) {
|
|
51
|
+
case "InsertQueryNode":
|
|
52
|
+
case "UpdateQueryNode":
|
|
53
|
+
case "DeleteQueryNode":
|
|
54
|
+
// Represents a RETURNING clause in a SQL statement.
|
|
55
|
+
const returning = {
|
|
56
|
+
kind: "ReturningNode",
|
|
57
|
+
selections: [],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// If the query already has a selection, then append it.
|
|
61
|
+
if (args.node.returning) {
|
|
62
|
+
returning.selections.push(...args.node.returning.selections);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Retrieve the audit context from async storage.
|
|
66
|
+
const audit = getAuditContext();
|
|
67
|
+
|
|
68
|
+
if (audit.identityId) {
|
|
69
|
+
const rawNode = sql`set_identity_id(${audit.identityId})`
|
|
70
|
+
.as(this.identityIdAlias)
|
|
71
|
+
.toOperationNode();
|
|
72
|
+
|
|
73
|
+
returning.selections.push(SelectionNode.create(rawNode));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (audit.traceId) {
|
|
77
|
+
const rawNode = sql`set_trace_id(${audit.traceId})`
|
|
78
|
+
.as(this.traceIdAlias)
|
|
79
|
+
.toOperationNode();
|
|
80
|
+
|
|
81
|
+
returning.selections.push(SelectionNode.create(rawNode));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...args.node,
|
|
86
|
+
returning: returning,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...args.node,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Drops the set_identity_id() and set_trace_id() fields from the result.
|
|
96
|
+
transformResult(args) {
|
|
97
|
+
if (args.result?.rows) {
|
|
98
|
+
for (let i = 0; i < args.result.rows.length; i++) {
|
|
99
|
+
delete args.result.rows[i][this.identityIdAlias];
|
|
100
|
+
delete args.result.rows[i][this.traceIdAlias];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return args.result;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports.withAuditContext = withAuditContext;
|
|
109
|
+
module.exports.getAuditContext = getAuditContext;
|
|
110
|
+
module.exports.AuditContextPlugin = AuditContextPlugin;
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { test, expect, beforeEach } from "vitest";
|
|
2
|
+
const { ModelAPI } = require("./ModelAPI");
|
|
3
|
+
const { PROTO_ACTION_TYPES } = require("./consts");
|
|
4
|
+
const { sql } = require("kysely");
|
|
5
|
+
const { useDatabase, withDatabase } = require("./database");
|
|
6
|
+
const KSUID = require("ksuid");
|
|
7
|
+
const TraceParent = require("traceparent");
|
|
8
|
+
const { withAuditContext } = require("./auditing");
|
|
9
|
+
|
|
10
|
+
let personAPI;
|
|
11
|
+
const db = useDatabase();
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
await sql`
|
|
15
|
+
|
|
16
|
+
DROP TABLE IF EXISTS post;
|
|
17
|
+
DROP TABLE IF EXISTS person;
|
|
18
|
+
DROP TABLE IF EXISTS author;
|
|
19
|
+
|
|
20
|
+
CREATE TABLE person(
|
|
21
|
+
id text PRIMARY KEY,
|
|
22
|
+
name text UNIQUE
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE OR REPLACE FUNCTION set_identity_id(id VARCHAR)
|
|
26
|
+
RETURNS TEXT AS $$
|
|
27
|
+
BEGIN
|
|
28
|
+
RETURN set_config('audit.identity_id', id, true);
|
|
29
|
+
END
|
|
30
|
+
$$ LANGUAGE plpgsql;
|
|
31
|
+
|
|
32
|
+
CREATE OR REPLACE FUNCTION set_trace_id(id VARCHAR)
|
|
33
|
+
RETURNS TEXT AS $$
|
|
34
|
+
BEGIN
|
|
35
|
+
RETURN set_config('audit.trace_id', id, true);
|
|
36
|
+
END
|
|
37
|
+
$$ LANGUAGE plpgsql;
|
|
38
|
+
`.execute(db);
|
|
39
|
+
|
|
40
|
+
personAPI = new ModelAPI("person", undefined, {});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function identityIdFromConfigParam(database, nonLocal = true) {
|
|
44
|
+
const result =
|
|
45
|
+
await sql`SELECT NULLIF(current_setting('audit.identity_id', ${sql.literal(
|
|
46
|
+
nonLocal
|
|
47
|
+
)}), '') AS id`.execute(database);
|
|
48
|
+
return result.rows[0].id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function traceIdFromConfigParam(database, nonLocal = true) {
|
|
52
|
+
const result =
|
|
53
|
+
await sql`SELECT NULLIF(current_setting('audit.trace_id', ${sql.literal(
|
|
54
|
+
nonLocal
|
|
55
|
+
)}), '') AS id`.execute(database);
|
|
56
|
+
return result.rows[0].id;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("auditing - capturing identity id in transaction", async () => {
|
|
60
|
+
const request = {
|
|
61
|
+
meta: {
|
|
62
|
+
identity: { id: KSUID.randomSync().string },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const identityId = request.meta.identity.id;
|
|
67
|
+
|
|
68
|
+
const row = await withDatabase(db, true, async ({ transaction }) => {
|
|
69
|
+
const row = withAuditContext(request, async () => {
|
|
70
|
+
return await personAPI.create({
|
|
71
|
+
id: KSUID.randomSync().string,
|
|
72
|
+
name: "James",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
77
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
78
|
+
|
|
79
|
+
return row;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(row.name).toEqual("James");
|
|
83
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
84
|
+
expect(await identityIdFromConfigParam(db, false)).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("auditing - capturing tracing in transaction", async () => {
|
|
88
|
+
const request = {
|
|
89
|
+
meta: {
|
|
90
|
+
tracing: {
|
|
91
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const traceId = TraceParent.fromString(
|
|
97
|
+
request.meta.tracing.traceparent
|
|
98
|
+
).traceId;
|
|
99
|
+
|
|
100
|
+
const row = await withDatabase(db, true, async ({ transaction }) => {
|
|
101
|
+
const row = withAuditContext(request, async () => {
|
|
102
|
+
return await personAPI.create({
|
|
103
|
+
id: KSUID.randomSync().string,
|
|
104
|
+
name: "Jim",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
109
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
110
|
+
|
|
111
|
+
return row;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(row.name).toEqual("Jim");
|
|
115
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
116
|
+
expect(await traceIdFromConfigParam(db, false)).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("auditing - capturing identity id without transaction", async () => {
|
|
120
|
+
const request = {
|
|
121
|
+
meta: {
|
|
122
|
+
identity: { id: KSUID.randomSync().string },
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const row = await withDatabase(db, false, async ({ sDb }) => {
|
|
127
|
+
const row = withAuditContext(request, async () => {
|
|
128
|
+
return await personAPI.create({
|
|
129
|
+
id: KSUID.randomSync().string,
|
|
130
|
+
name: "James",
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(await identityIdFromConfigParam(sDb)).toBeNull();
|
|
135
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
136
|
+
|
|
137
|
+
return row;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(row.name).toEqual("James");
|
|
141
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
142
|
+
expect(await identityIdFromConfigParam(db, false)).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("auditing - capturing tracing without transaction", async () => {
|
|
146
|
+
const request = {
|
|
147
|
+
meta: {
|
|
148
|
+
tracing: {
|
|
149
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const row = await withDatabase(db, false, async ({ sDb }) => {
|
|
155
|
+
const row = withAuditContext(request, async () => {
|
|
156
|
+
return await personAPI.create({
|
|
157
|
+
id: KSUID.randomSync().string,
|
|
158
|
+
name: "Jim",
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(await traceIdFromConfigParam(sDb)).toBeNull();
|
|
163
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
164
|
+
|
|
165
|
+
return row;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(row.name).toEqual("Jim");
|
|
169
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
170
|
+
expect(await traceIdFromConfigParam(db, false)).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("auditing - no audit context", async () => {
|
|
174
|
+
const row = await withDatabase(
|
|
175
|
+
db,
|
|
176
|
+
PROTO_ACTION_TYPES.CREATE,
|
|
177
|
+
async ({ transaction }) => {
|
|
178
|
+
const row = withAuditContext({}, async () => {
|
|
179
|
+
return await personAPI.create({
|
|
180
|
+
id: KSUID.randomSync().string,
|
|
181
|
+
name: "Jake",
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(await identityIdFromConfigParam(transaction)).toBeNull();
|
|
186
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
187
|
+
expect(await traceIdFromConfigParam(transaction)).toBeNull();
|
|
188
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
189
|
+
return row;
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(KSUID.parse(row.id).string).toEqual(row.id);
|
|
194
|
+
expect(row.name).toEqual("Jake");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("auditing - ModelAPI.create", async () => {
|
|
198
|
+
const request = {
|
|
199
|
+
meta: {
|
|
200
|
+
identity: { id: KSUID.randomSync().string },
|
|
201
|
+
tracing: {
|
|
202
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const identityId = request.meta.identity.id;
|
|
208
|
+
const traceId = TraceParent.fromString(
|
|
209
|
+
request.meta.tracing.traceparent
|
|
210
|
+
).traceId;
|
|
211
|
+
|
|
212
|
+
const row = await withDatabase(db, true, async ({ transaction }) => {
|
|
213
|
+
const row = withAuditContext(request, async () => {
|
|
214
|
+
return await personAPI.create({
|
|
215
|
+
id: KSUID.randomSync().string,
|
|
216
|
+
name: "Jake",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
221
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
222
|
+
|
|
223
|
+
return row;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(row.name).toEqual("Jake");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("auditing - ModelAPI.update", async () => {
|
|
230
|
+
const request = {
|
|
231
|
+
meta: {
|
|
232
|
+
identity: { id: KSUID.randomSync().string },
|
|
233
|
+
tracing: {
|
|
234
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const identityId = request.meta.identity.id;
|
|
240
|
+
const traceId = TraceParent.fromString(
|
|
241
|
+
request.meta.tracing.traceparent
|
|
242
|
+
).traceId;
|
|
243
|
+
|
|
244
|
+
const created = await personAPI.create({
|
|
245
|
+
id: KSUID.randomSync().string,
|
|
246
|
+
name: "Jake",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const row = await withDatabase(db, true, async ({ transaction }) => {
|
|
250
|
+
const row = withAuditContext(request, async () => {
|
|
251
|
+
return await personAPI.update({ id: created.id }, { name: "Jim" });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
255
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
256
|
+
|
|
257
|
+
return row;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(row.name).toEqual("Jim");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("auditing - ModelAPI.delete", async () => {
|
|
264
|
+
const request = {
|
|
265
|
+
meta: {
|
|
266
|
+
identity: { id: KSUID.randomSync().string },
|
|
267
|
+
tracing: {
|
|
268
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const identityId = request.meta.identity.id;
|
|
274
|
+
const traceId = TraceParent.fromString(
|
|
275
|
+
request.meta.tracing.traceparent
|
|
276
|
+
).traceId;
|
|
277
|
+
|
|
278
|
+
const created = await personAPI.create({
|
|
279
|
+
id: KSUID.randomSync().string,
|
|
280
|
+
name: "Jake",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const row = await withDatabase(db, true, async ({ transaction }) => {
|
|
284
|
+
const row = withAuditContext(request, async () => {
|
|
285
|
+
return await personAPI.delete({ id: created.id });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
289
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
290
|
+
|
|
291
|
+
return row;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(row).toEqual(created.id);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("auditing - identity id and trace id fields dropped from result", async () => {
|
|
298
|
+
const request = {
|
|
299
|
+
meta: {
|
|
300
|
+
identity: { id: KSUID.randomSync().string },
|
|
301
|
+
tracing: {
|
|
302
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const identityId = request.meta.identity.id;
|
|
308
|
+
const traceId = TraceParent.fromString(
|
|
309
|
+
request.meta.tracing.traceparent
|
|
310
|
+
).traceId;
|
|
311
|
+
|
|
312
|
+
const row = await withDatabase(db, true, async ({ transaction }) => {
|
|
313
|
+
const row = withAuditContext(request, async () => {
|
|
314
|
+
return await personAPI.create({
|
|
315
|
+
id: KSUID.randomSync().string,
|
|
316
|
+
name: "Jake",
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
321
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
322
|
+
|
|
323
|
+
return row;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(row.name).toEqual("Jake");
|
|
327
|
+
expect(row.keelIdentityId).toBeUndefined();
|
|
328
|
+
expect(row.keelTraceId).toBeUndefined();
|
|
329
|
+
expect(Object.keys(row).length).toEqual(2);
|
|
330
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const { CamelCasePlugin } = require("kysely");
|
|
2
|
+
const { isPlainObject, isRichType } = require("./type-utils");
|
|
3
|
+
|
|
4
|
+
// KeelCamelCasePlugin is a wrapper around kysely's CamelCasePlugin. The behaviour is the same apart from the fact that
|
|
5
|
+
// nested objects that are of a rich keel data type, such as Duration, are skipped so that they continue to be
|
|
6
|
+
// implementations of the rich data classes defined by Keel.
|
|
7
|
+
class KeelCamelCasePlugin {
|
|
8
|
+
constructor(opt) {
|
|
9
|
+
this.opt = opt;
|
|
10
|
+
this.CamelCasePlugin = new CamelCasePlugin(opt);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
transformQuery(args) {
|
|
14
|
+
return this.CamelCasePlugin.transformQuery(args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async transformResult(args) {
|
|
18
|
+
if (args.result.rows && Array.isArray(args.result.rows)) {
|
|
19
|
+
return {
|
|
20
|
+
...args.result,
|
|
21
|
+
rows: args.result.rows.map((row) => this.mapRow(row)),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return args.result;
|
|
25
|
+
}
|
|
26
|
+
mapRow(row) {
|
|
27
|
+
return Object.keys(row).reduce((obj, key) => {
|
|
28
|
+
// Fields using @sequence will have a corresponding __sequence field which we drop as we don't want to return it
|
|
29
|
+
if (key.endsWith("__sequence")) {
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
let value = row[key];
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
value = value.map((it) =>
|
|
35
|
+
canMap(it, this.opt) ? this.mapRow(it) : it
|
|
36
|
+
);
|
|
37
|
+
} else if (canMap(value, this.opt)) {
|
|
38
|
+
value = this.mapRow(value);
|
|
39
|
+
}
|
|
40
|
+
obj[this.CamelCasePlugin.camelCase(key)] = value;
|
|
41
|
+
return obj;
|
|
42
|
+
}, {});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canMap(obj, opt) {
|
|
47
|
+
return (
|
|
48
|
+
isPlainObject(obj) && !opt?.maintainNestedObjectKeys && !isRichType(obj)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports.KeelCamelCasePlugin = KeelCamelCasePlugin;
|
package/src/casing.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { snakeCase, camelCase } = require("change-case");
|
|
2
|
+
|
|
3
|
+
function camelCaseObject(obj = {}) {
|
|
4
|
+
const r = {};
|
|
5
|
+
for (const key of Object.keys(obj)) {
|
|
6
|
+
r[
|
|
7
|
+
camelCase(key, {
|
|
8
|
+
transform: camelCaseTransform,
|
|
9
|
+
splitRegexp: [
|
|
10
|
+
/([a-z0-9])([A-Z])/g,
|
|
11
|
+
/([A-Z])([A-Z][a-z])/g,
|
|
12
|
+
/([a-zA-Z])([0-9])/g,
|
|
13
|
+
],
|
|
14
|
+
})
|
|
15
|
+
] = obj[key];
|
|
16
|
+
}
|
|
17
|
+
return r;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function snakeCaseObject(obj) {
|
|
21
|
+
const r = {};
|
|
22
|
+
for (const key of Object.keys(obj)) {
|
|
23
|
+
r[
|
|
24
|
+
snakeCase(key, {
|
|
25
|
+
splitRegexp: [
|
|
26
|
+
/([a-z0-9])([A-Z])/g,
|
|
27
|
+
/([A-Z])([A-Z][a-z])/g,
|
|
28
|
+
/([a-zA-Z])([0-9])/g,
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
] = obj[key];
|
|
32
|
+
}
|
|
33
|
+
return r;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function upperCamelCase(s) {
|
|
37
|
+
s = camelCase(s);
|
|
38
|
+
return s[0].toUpperCase() + s.substring(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function camelCaseTransform(input, index) {
|
|
42
|
+
if (index === 0) return input.toLowerCase();
|
|
43
|
+
const firstChar = input.charAt(0);
|
|
44
|
+
const lowerChars = input.substr(1).toLowerCase();
|
|
45
|
+
return `${firstChar.toUpperCase()}${lowerChars}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
camelCaseObject,
|
|
50
|
+
snakeCaseObject,
|
|
51
|
+
snakeCase,
|
|
52
|
+
camelCase,
|
|
53
|
+
upperCamelCase,
|
|
54
|
+
};
|