@teamkeel/functions-runtime 0.398.0 → 0.399.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamkeel/functions-runtime",
3
- "version": "0.398.0",
3
+ "version": "0.399.1",
4
4
  "description": "Internal package used by @teamkeel/sdk",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -33,6 +33,7 @@
33
33
  "ksuid": "^3.0.0",
34
34
  "kysely": "~0.25.0",
35
35
  "pg": "^8.13.1",
36
+ "postgres-interval": "^4.0.2",
36
37
  "traceparent": "^1.0.0",
37
38
  "ws": "^8.18.0"
38
39
  },
@@ -0,0 +1,40 @@
1
+ const parseInterval = require("postgres-interval");
2
+
3
+ const isoRegex =
4
+ /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
5
+
6
+ class Duration {
7
+ constructor(postgresString) {
8
+ this._typename = "Duration";
9
+ this.pgInterval = postgresString;
10
+ this._interval = parseInterval(postgresString);
11
+ }
12
+
13
+ static fromISOString(isoString) {
14
+ // todo parse iso string to postgres string
15
+ const match = isoString.match(isoRegex);
16
+ if (match) {
17
+ let d = new Duration();
18
+ d._interval.years = match[1];
19
+ d._interval.months = match[2];
20
+ d._interval.days = match[3];
21
+ d._interval.hours = match[4];
22
+ d._interval.minutes = match[5];
23
+ d._interval.seconds = match[6];
24
+ return d;
25
+ }
26
+ return new Duration();
27
+ }
28
+
29
+ toISOString() {
30
+ return this._interval.toISOStringShort();
31
+ }
32
+
33
+ toPostgres() {
34
+ return this._interval.toPostgres();
35
+ }
36
+ }
37
+
38
+ module.exports = {
39
+ Duration,
40
+ };
@@ -0,0 +1,34 @@
1
+ import { test, expect } from "vitest";
2
+ const { Duration } = require("./Duration");
3
+
4
+ test("fromISOString test", async () => {
5
+ const fullDate = Duration.fromISOString("P1Y2M3DT4H5M6S");
6
+ expect(fullDate.toISOString()).toEqual("P1Y2M3DT4H5M6S");
7
+ expect(fullDate.toPostgres()).toEqual(
8
+ "1 years 2 months 3 days 4 hours 5 minutes 6 seconds"
9
+ );
10
+ const dateOnly = Duration.fromISOString("P2Y3M4D");
11
+ expect(dateOnly.toISOString()).toEqual("P2Y3M4D");
12
+ expect(dateOnly.toPostgres()).toEqual("2 years 3 months 4 days");
13
+ const timeOnly = Duration.fromISOString("PT4H5M6S");
14
+ expect(timeOnly.toISOString()).toEqual("PT4H5M6S");
15
+ expect(timeOnly.toPostgres()).toEqual("4 hours 5 minutes 6 seconds");
16
+ const years = Duration.fromISOString("P10Y");
17
+ expect(years.toISOString()).toEqual("P10Y");
18
+ expect(years.toPostgres()).toEqual("10 years");
19
+ const months = Duration.fromISOString("P20M");
20
+ expect(months.toISOString()).toEqual("P20M");
21
+ expect(months.toPostgres()).toEqual("20 months");
22
+ const days = Duration.fromISOString("P31D");
23
+ expect(days.toISOString()).toEqual("P31D");
24
+ expect(days.toPostgres()).toEqual("31 days");
25
+ const hours = Duration.fromISOString("PT4H");
26
+ expect(hours.toISOString()).toEqual("PT4H");
27
+ expect(hours.toPostgres()).toEqual("4 hours");
28
+ const minutes = Duration.fromISOString("PT61M");
29
+ expect(minutes.toISOString()).toEqual("PT61M");
30
+ expect(minutes.toPostgres()).toEqual("61 minutes");
31
+ const seconds = Duration.fromISOString("PT76S");
32
+ expect(seconds.toISOString()).toEqual("PT76S");
33
+ expect(seconds.toPostgres()).toEqual("76 seconds");
34
+ });
package/src/ModelAPI.js CHANGED
@@ -2,14 +2,15 @@ const { sql } = require("kysely");
2
2
  const { useDatabase } = require("./database");
3
3
  const {
4
4
  transformRichDataTypes,
5
- isPlainObject,
6
5
  isReferencingExistingRecord,
7
6
  } = require("./parsing");
7
+ const { isPlainObject } = require("./type-utils");
8
8
  const { QueryBuilder } = require("./QueryBuilder");
9
9
  const { QueryContext } = require("./QueryContext");
10
10
  const { applyWhereConditions } = require("./applyWhereConditions");
11
11
  const { applyJoins } = require("./applyJoins");
12
12
  const { InlineFile, File } = require("./File");
13
+ const { Duration } = require("./Duration");
13
14
 
14
15
  const {
15
16
  applyLimit,
@@ -163,6 +164,9 @@ class ModelAPI {
163
164
 
164
165
  for (const key of keys) {
165
166
  const value = values[key];
167
+ if (value instanceof Duration) {
168
+ row[key] = value.toPostgres();
169
+ }
166
170
  // handle files that need uploading
167
171
  if (value instanceof InlineFile) {
168
172
  const storedFile = await value.store();
@@ -249,6 +253,9 @@ async function create(conn, tableName, tableConfigs, values) {
249
253
  const columnConfig = tableConfig[key];
250
254
 
251
255
  if (!columnConfig) {
256
+ if (value instanceof Duration) {
257
+ row[key] = value.toPostgres();
258
+ }
252
259
  if (value instanceof InlineFile) {
253
260
  const storedFile = await value.store();
254
261
  row[key] = storedFile.toDbRecord();
@@ -0,0 +1,48 @@
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
+ let value = row[key];
29
+ if (Array.isArray(value)) {
30
+ value = value.map((it) =>
31
+ canMap(it, this.opt) ? this.mapRow(it) : it
32
+ );
33
+ } else if (canMap(value, this.opt)) {
34
+ value = this.mapRow(value);
35
+ }
36
+ obj[this.CamelCasePlugin.camelCase(key)] = value;
37
+ return obj;
38
+ }, {});
39
+ }
40
+ }
41
+
42
+ function canMap(obj, opt) {
43
+ return (
44
+ isPlainObject(obj) && !opt?.maintainNestedObjectKeys && !isRichType(obj)
45
+ );
46
+ }
47
+
48
+ module.exports.KeelCamelCasePlugin = KeelCamelCasePlugin;
package/src/database.js CHANGED
@@ -1,11 +1,13 @@
1
- const { Kysely, PostgresDialect, CamelCasePlugin } = require("kysely");
1
+ const { Kysely, PostgresDialect } = require("kysely");
2
2
  const neonserverless = require("@neondatabase/serverless");
3
3
  const { AsyncLocalStorage } = require("async_hooks");
4
4
  const { AuditContextPlugin } = require("./auditing");
5
+ const { KeelCamelCasePlugin } = require("./camelCasePlugin");
5
6
  const pg = require("pg");
6
7
  const { withSpan } = require("./tracing");
7
8
  const ws = require("ws");
8
9
  const fs = require("node:fs");
10
+ const { Duration } = require("./Duration");
9
11
 
10
12
  // withDatabase is responsible for setting the correct database client in our AsyncLocalStorage
11
13
  // so that the the code in a custom function uses the correct client.
@@ -74,10 +76,9 @@ function createDatabaseClient({ connString } = {}) {
74
76
  new AuditContextPlugin(),
75
77
  // allows users to query using camelCased versions of the database column names, which
76
78
  // should match the names we use in our schema.
77
- // https://kysely-org.github.io/kysely/classes/CamelCasePlugin.html
78
- // If they don't, then we can create a custom implementation of the plugin where we control
79
- // the casing behaviour (see url above for example)
80
- new CamelCasePlugin(),
79
+ // We're using an extended version of Kysely's CamelCasePlugin which avoids changing keys of objects that represent
80
+ // rich data formats, specific to Keel (e.g. Duration)
81
+ new KeelCamelCasePlugin(),
81
82
  ],
82
83
  log(event) {
83
84
  if ("DEBUG" in process.env) {
@@ -146,9 +147,14 @@ function getDialect(connString) {
146
147
  case "pg":
147
148
  // Adding a custom type parser for numeric fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
148
149
  // 1700 = type for NUMERIC
149
- pg.types.setTypeParser(1700, function (val) {
150
+ pg.types.setTypeParser(pg.types.builtins.NUMERIC, function (val) {
150
151
  return parseFloat(val);
151
152
  });
153
+ // Adding a custom type parser for interval fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
154
+ // 1186 = type for INTERVAL
155
+ pg.types.setTypeParser(pg.types.builtins.INTERVAL, function (val) {
156
+ return new Duration(val);
157
+ });
152
158
 
153
159
  return new PostgresDialect({
154
160
  pool: new InstrumentedPool({
@@ -179,9 +185,20 @@ function getDialect(connString) {
179
185
  case "neon":
180
186
  // Adding a custom type parser for numeric fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
181
187
  // 1700 = type for NUMERIC
182
- neonserverless.types.setTypeParser(1700, function (val) {
183
- return parseFloat(val);
184
- });
188
+ neonserverless.types.setTypeParser(
189
+ pg.types.builtins.NUMERIC,
190
+ function (val) {
191
+ return parseFloat(val);
192
+ }
193
+ );
194
+ // Adding a custom type parser for interval fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
195
+ // 1186 = type for INTERVAL
196
+ neonserverless.types.setTypeParser(
197
+ pg.types.builtins.INTERVAL,
198
+ function (val) {
199
+ return new Duration(val);
200
+ }
201
+ );
185
202
 
186
203
  neonserverless.neonConfig.webSocketConstructor = ws;
187
204
 
package/src/index.d.ts CHANGED
@@ -27,6 +27,15 @@ export type NumberWhereCondition = {
27
27
  notEquals?: number | null;
28
28
  };
29
29
 
30
+ export type DurationWhereCondition = {
31
+ greaterThan?: DurationString | null;
32
+ greaterThanOrEquals?: DurationString | null;
33
+ lessThan?: DurationString | null;
34
+ lessThanOrEquals?: DurationString | null;
35
+ equals?: DurationString | null;
36
+ notEquals?: DurationString | null;
37
+ };
38
+
30
39
  export type DateWhereCondition = {
31
40
  equals?: Date | string | null;
32
41
  equalsRelative?: RelativeDateString | null;
@@ -197,6 +206,14 @@ export type FileDbRecord = {
197
206
  size: number;
198
207
  };
199
208
 
209
+ export declare class Duration {
210
+ constructor(postgresString: string);
211
+ static fromISOString(iso: DurationString): Duration;
212
+
213
+ toISOString(): DurationString;
214
+ toPostgres(): string;
215
+ }
216
+
200
217
  export type SortDirection = "asc" | "desc" | "ASC" | "DESC";
201
218
 
202
219
  // Request headers cannot be mutated, so remove any methods that mutate
@@ -272,3 +289,26 @@ export type RelativeDateString =
272
289
  | `${direction} ${unit}`
273
290
  | `${direction} ${value} ${unit}`
274
291
  | `${direction} ${value} ${completed} ${unit}`;
292
+
293
+ type dateDuration =
294
+ | `${number}Y${number}M${number}D` // Example: 1Y2M10D
295
+ | `${number}Y${number}M` // Example: 1Y2M
296
+ | `${number}Y${number}D` // Example: 1Y10D
297
+ | `${number}M${number}D` // Example: 10M2D
298
+ | `${number}Y` // Example: 1Y
299
+ | `${number}M` // Example: 1M
300
+ | `${number}D`; // Example: 2D
301
+
302
+ type timeDuration =
303
+ | `${number}H${number}M${number}S` // Example: 2H30M
304
+ | `${number}H${number}M` // Example: 2H30M
305
+ | `${number}M${number}S` // Example: 2M30S
306
+ | `${number}H${number}S` // Example: 2H30S
307
+ | `${number}H` // Example: 2H
308
+ | `${number}M` // Example: 30M
309
+ | `${number}S`; // Example: 30S
310
+
311
+ export type DurationString =
312
+ | `P${dateDuration}T${timeDuration}`
313
+ | `P${dateDuration}`
314
+ | `PT${timeDuration}`;
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  } = require("./permissions");
13
13
  const tracing = require("./tracing");
14
14
  const { InlineFile, File } = require("./File");
15
+ const { Duration } = require("./Duration");
15
16
  const { ErrorPresets } = require("./errors");
16
17
 
17
18
  module.exports = {
@@ -21,6 +22,7 @@ module.exports = {
21
22
  handleJob,
22
23
  handleSubscriber,
23
24
  useDatabase,
25
+ Duration,
24
26
  InlineFile,
25
27
  File,
26
28
  Permissions,
package/src/parsing.js CHANGED
@@ -1,4 +1,6 @@
1
+ const { Duration } = require("./Duration");
1
2
  const { InlineFile, File } = require("./File");
3
+ const { isPlainObject } = require("./type-utils");
2
4
 
3
5
  // parseInputs takes a set of inputs and creates objects for the ones that are of a complex type.
4
6
  //
@@ -13,6 +15,9 @@ function parseInputs(inputs) {
13
15
  case "InlineFile":
14
16
  inputs[k] = InlineFile.fromDataURL(inputs[k].dataURL);
15
17
  break;
18
+ case "Duration":
19
+ inputs[k] = Duration.fromISOString(inputs[k].interval);
20
+ break;
16
21
  default:
17
22
  break;
18
23
  }
@@ -36,6 +41,8 @@ async function parseOutputs(inputs) {
36
41
  if (inputs[k] instanceof InlineFile) {
37
42
  const stored = await inputs[k].store();
38
43
  inputs[k] = stored;
44
+ } else if (inputs[k] instanceof Duration) {
45
+ inputs[k] = inputs[k].toISOString();
39
46
  } else {
40
47
  inputs[k] = await parseOutputs(inputs[k]);
41
48
  }
@@ -54,7 +61,14 @@ function transformRichDataTypes(data) {
54
61
  for (const key of keys) {
55
62
  const value = data[key];
56
63
  if (isPlainObject(value)) {
57
- if (value.key && value.size && value.filename && value.contentType) {
64
+ if (value._typename == "Duration" && value.pgInterval) {
65
+ row[key] = new Duration(value.pgInterval);
66
+ } else if (
67
+ value.key &&
68
+ value.size &&
69
+ value.filename &&
70
+ value.contentType
71
+ ) {
58
72
  row[key] = File.fromDbRecord(value);
59
73
  } else {
60
74
  row[key] = value;
@@ -68,10 +82,6 @@ function transformRichDataTypes(data) {
68
82
  return row;
69
83
  }
70
84
 
71
- function isPlainObject(obj) {
72
- return Object.prototype.toString.call(obj) === "[object Object]";
73
- }
74
-
75
85
  function isReferencingExistingRecord(value) {
76
86
  return Object.keys(value).length === 1 && value.id;
77
87
  }
@@ -80,6 +90,5 @@ module.exports = {
80
90
  parseInputs,
81
91
  parseOutputs,
82
92
  transformRichDataTypes,
83
- isPlainObject,
84
93
  isReferencingExistingRecord,
85
94
  };
@@ -0,0 +1,18 @@
1
+ const { Duration } = require("./Duration");
2
+
3
+ function isPlainObject(obj) {
4
+ return Object.prototype.toString.call(obj) === "[object Object]";
5
+ }
6
+
7
+ function isRichType(obj) {
8
+ if (!isPlainObject(obj)) {
9
+ return false;
10
+ }
11
+
12
+ return obj instanceof Duration;
13
+ }
14
+
15
+ module.exports = {
16
+ isPlainObject,
17
+ isRichType,
18
+ };