@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.
Files changed (49) hide show
  1. package/.env.test +2 -0
  2. package/compose.yaml +10 -0
  3. package/package.json +5 -23
  4. package/src/Duration.js +40 -0
  5. package/src/Duration.test.js +34 -0
  6. package/src/File.js +295 -0
  7. package/src/ModelAPI.js +377 -0
  8. package/src/ModelAPI.test.js +1428 -0
  9. package/src/QueryBuilder.js +184 -0
  10. package/src/QueryContext.js +90 -0
  11. package/src/RequestHeaders.js +21 -0
  12. package/src/TimePeriod.js +89 -0
  13. package/src/TimePeriod.test.js +148 -0
  14. package/src/applyAdditionalQueryConstraints.js +22 -0
  15. package/src/applyJoins.js +67 -0
  16. package/src/applyWhereConditions.js +124 -0
  17. package/src/auditing.js +110 -0
  18. package/src/auditing.test.js +330 -0
  19. package/src/camelCasePlugin.js +52 -0
  20. package/src/casing.js +54 -0
  21. package/src/casing.test.js +56 -0
  22. package/src/consts.js +14 -0
  23. package/src/database.js +244 -0
  24. package/src/errors.js +160 -0
  25. package/src/handleJob.js +110 -0
  26. package/src/handleJob.test.js +270 -0
  27. package/src/handleRequest.js +153 -0
  28. package/src/handleRequest.test.js +463 -0
  29. package/src/handleRoute.js +112 -0
  30. package/src/handleSubscriber.js +105 -0
  31. package/src/index.d.ts +317 -0
  32. package/src/index.js +38 -0
  33. package/src/parsing.js +113 -0
  34. package/src/parsing.test.js +140 -0
  35. package/src/permissions.js +77 -0
  36. package/src/permissions.test.js +118 -0
  37. package/src/tracing.js +184 -0
  38. package/src/tracing.test.js +147 -0
  39. package/src/tryExecuteFunction.js +91 -0
  40. package/src/tryExecuteJob.js +29 -0
  41. package/src/tryExecuteSubscriber.js +17 -0
  42. package/src/type-utils.js +18 -0
  43. package/vite.config.js +7 -0
  44. package/dist/index.d.mts +0 -340
  45. package/dist/index.d.ts +0 -340
  46. package/dist/index.js +0 -3093
  47. package/dist/index.js.map +0 -1
  48. package/dist/index.mjs +0 -3097
  49. 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
+ };
@@ -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
+ };