@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 +2 -1
- package/src/Duration.js +40 -0
- package/src/Duration.test.js +34 -0
- package/src/ModelAPI.js +8 -1
- package/src/camelCasePlugin.js +48 -0
- package/src/database.js +26 -9
- package/src/index.d.ts +40 -0
- package/src/index.js +2 -0
- package/src/parsing.js +15 -6
- package/src/type-utils.js +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamkeel/functions-runtime",
|
|
3
|
-
"version": "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
|
},
|
package/src/Duration.js
ADDED
|
@@ -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
|
|
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
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
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(
|
|
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(
|
|
183
|
-
|
|
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.
|
|
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
|
+
};
|