@rwillians/exto 0.1.2 → 0.2.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.
@@ -3,7 +3,7 @@
3
3
  // STANDARD SCHEMA SPEC //
4
4
  // // // // // // // // // // // // // // // // // // // // // // // //
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.uuid = exports.strictObject = exports.string = exports.optional = exports.number = exports.nullable = exports.isoDate = exports.integer = exports.instanceOf = exports.enumOf = exports.date = exports.boolean = exports.bigint = exports.array = exports.parseOrThrow = exports.parse = exports.ValidationError = void 0;
6
+ exports.uuid = exports.strictObject = exports.string = exports.record = exports.optional = exports.number = exports.nullable = exports.isoDate = exports.integer = exports.instanceOf = exports.enumOf = exports.date = exports.boolean = exports.bigint = exports.array = exports.parseOrThrow = exports.parse = exports.ValidationError = void 0;
7
7
  /**
8
8
  * @public Thrown when a value fails validation, carrying every issue found.
9
9
  * @since 0.1.0
@@ -192,6 +192,15 @@ const optional = (schema) => define((input) => input === undefined
192
192
  ? { value: undefined }
193
193
  : (0, exports.parse)(schema, input));
194
194
  exports.optional = optional;
195
+ /**
196
+ * @public Defines a standard schema for plain objects with string keys.
197
+ * @since 0.2.0
198
+ * @version 1
199
+ */
200
+ const record = () => define((input) => typeof input === 'object' && input !== null && Object.getPrototypeOf(input) === Object.prototype
201
+ ? { value: input }
202
+ : fail('must be a plain object'));
203
+ exports.record = record;
195
204
  /**
196
205
  * @public Defines a standard schema for strings, optionally limited in
197
206
  * length.
@@ -1,27 +1,47 @@
1
1
  import { SQL } from 'bun';
2
- import { raise } from './utils';
3
2
  const TYPES = {
4
3
  BIGINT: () => 'BIGINT',
5
4
  BINARY: () => 'BYTEA',
6
5
  BOOLEAN: () => 'BOOLEAN',
7
6
  CITEXT: () => 'CITEXT',
8
7
  DATE: () => 'DATE',
9
- DATETIME: () => 'TIMESTAMPTZ',
10
- DECIMAL: ({ precision, scale }) => [precision, scale].every(Number.isInteger) ? `NUMERIC(${precision}, ${scale})`
11
- : [precision, scale].some(Number.isInteger) ? raise('precision and scale are mutually inclusive, either you provide both or neither')
12
- : 'NUMERIC',
8
+ DECIMAL: ({ precision, scale }) => precision !== undefined && scale !== undefined ? `NUMERIC(${precision}, ${scale})` : 'NUMERIC',
13
9
  FLOAT: () => 'DOUBLE PRECISION',
14
10
  INTEGER: () => 'BIGINT',
15
11
  JSON: () => 'JSONB',
16
12
  SEQUENCE: () => 'BIGINT',
17
13
  STRING: ({ length = 255 }) => `VARCHAR(${length})`,
18
14
  TEXT: () => 'TEXT',
15
+ TIMESTAMP: () => 'TIMESTAMPTZ',
19
16
  UUID: () => 'UUID',
20
17
  };
21
18
  const identity = () => ({
22
19
  encode: (value) => value,
23
20
  decode: (value) => value,
24
21
  });
22
+ // Bun.SQL reads bigints (BIGINT columns, COUNT and SUM) and NUMERICs as
23
+ // strings
24
+ const numeric = {
25
+ encode: (value) => value,
26
+ decode: (value) => Number(value),
27
+ };
28
+ // `#>>` reads text, so fields of other types are cast back to theirs
29
+ const CASTS = {
30
+ BIGINT: 'BIGINT',
31
+ BOOLEAN: 'BOOLEAN',
32
+ DECIMAL: 'NUMERIC',
33
+ FLOAT: 'DOUBLE PRECISION',
34
+ INTEGER: 'DOUBLE PRECISION',
35
+ SEQUENCE: 'BIGINT',
36
+ };
37
+ const jsonPath = (path) => `'{${path.map(key => `"${key.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\'', '\'\'')}"`).join(',')}}'`;
38
+ const json = (sql, path, type) => {
39
+ if (type === 'JSON')
40
+ return `${sql} #> ${jsonPath(path)}`;
41
+ const text = `${sql} #>> ${jsonPath(path)}`;
42
+ return CASTS[type] ? `(${text})::${CASTS[type]}` : text;
43
+ };
44
+ // Bun.SQL sends objects and arrays as JSON, and reads JSONB already parsed
25
45
  const dialect = {
26
46
  name: 'postgres',
27
47
  placeholder: (index) => `$${index}`,
@@ -29,20 +49,21 @@ const dialect = {
29
49
  generated: (col) => col.type === 'SEQUENCE'
30
50
  ? 'GENERATED ALWAYS AS IDENTITY'
31
51
  : undefined,
52
+ json,
32
53
  codecs: {
33
54
  BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
34
- BINARY: identity(),
55
+ BINARY: { encode: (value) => value, decode: (value) => new Uint8Array(value) },
35
56
  BOOLEAN: identity(),
36
57
  CITEXT: identity(),
37
58
  DATE: { encode: (value) => value, decode: (value) => value instanceof Date ? value.toISOString().slice(0, 10) : value },
38
- DATETIME: identity(),
39
- DECIMAL: { encode: (value) => value, decode: (value) => Number(value) },
40
- FLOAT: identity(),
41
- INTEGER: { encode: (value) => value, decode: (value) => Number(value) },
59
+ DECIMAL: numeric,
60
+ FLOAT: numeric,
61
+ INTEGER: numeric,
42
62
  JSON: identity(),
43
- SEQUENCE: { encode: (value) => value, decode: (value) => Number(value) },
63
+ SEQUENCE: numeric,
44
64
  STRING: identity(),
45
65
  TEXT: identity(),
66
+ TIMESTAMP: { encode: (value) => value, decode: (value) => new Date(value) },
46
67
  UUID: identity(),
47
68
  },
48
69
  };
@@ -59,6 +80,10 @@ class BunPostgres {
59
80
  async execute(sql, params) {
60
81
  return await this.sql.unsafe(sql, params);
61
82
  }
83
+ async close() {
84
+ await this.sql.close();
85
+ }
86
+ // nested transactions are savepoints of the outer one
62
87
  async transaction(fn) {
63
88
  const callback = (tx) => fn(new BunPostgres(tx, this.loggers, true));
64
89
  const result = this.inTransaction
@@ -5,7 +5,6 @@ const TYPES = {
5
5
  BOOLEAN: 'INTEGER',
6
6
  CITEXT: 'TEXT COLLATE NOCASE',
7
7
  DATE: 'TEXT',
8
- DATETIME: 'INTEGER',
9
8
  DECIMAL: 'NUMERIC',
10
9
  FLOAT: 'REAL',
11
10
  INTEGER: 'INTEGER',
@@ -13,6 +12,7 @@ const TYPES = {
13
12
  SEQUENCE: 'INTEGER',
14
13
  STRING: 'TEXT',
15
14
  TEXT: 'TEXT',
15
+ TIMESTAMP: 'INTEGER',
16
16
  UUID: 'TEXT',
17
17
  };
18
18
  const identity = () => ({
@@ -25,6 +25,8 @@ const numeric = {
25
25
  encode: (value) => value,
26
26
  decode: (value) => Number(value),
27
27
  };
28
+ // sqlite's JSON paths quote each key, so that any key is a valid one
29
+ const jsonPath = (path) => `'$${path.map(key => `."${key.replaceAll('"', '\\"').replaceAll('\'', '\'\'')}"`).join('')}'`;
28
30
  const dialect = {
29
31
  name: 'sqlite',
30
32
  placeholder: () => '?',
@@ -36,13 +38,13 @@ const dialect = {
36
38
  throw new Error(`SQLite can only generate sequences for primary keys, column "${col.name}" isn't one`);
37
39
  return undefined;
38
40
  },
41
+ json: (sql, path, type) => `${sql} ${type === 'JSON' ? '->' : '->>'} ${jsonPath(path)}`,
39
42
  codecs: {
40
43
  BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
41
- BINARY: identity(),
44
+ BINARY: { encode: (value) => value, decode: (value) => new Uint8Array(value) },
42
45
  BOOLEAN: { encode: (value) => (value ? 1 : 0), decode: (value) => Number(value) === 1 },
43
46
  CITEXT: identity(),
44
47
  DATE: identity(),
45
- DATETIME: { encode: (value) => value.getTime(), decode: (value) => new Date(Number(value)) },
46
48
  DECIMAL: numeric,
47
49
  FLOAT: numeric,
48
50
  INTEGER: numeric,
@@ -50,6 +52,7 @@ const dialect = {
50
52
  SEQUENCE: numeric,
51
53
  STRING: identity(),
52
54
  TEXT: identity(),
55
+ TIMESTAMP: { encode: (value) => value.getTime(), decode: (value) => new Date(Number(value)) },
53
56
  UUID: identity(),
54
57
  },
55
58
  };
@@ -67,6 +70,9 @@ class BunSQLite {
67
70
  // without safe integers, bun:sqlite rounds anything beyond 2^53
68
71
  return this.conn.prepare(sql).safeIntegers(true).all(...params);
69
72
  }
73
+ async close() {
74
+ this.conn.close();
75
+ }
70
76
  // bun:sqlite's own `transaction()` is synchronous and would commit before an
71
77
  // async callback settles, hence the manual BEGIN/COMMIT (SAVEPOINT if nested)
72
78
  async transaction(fn) {