@rwillians/exto 0.1.1 → 0.1.2

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.
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ // // // // // // // // // // // // // // // // // // // // // // // //
3
+ // STANDARD SCHEMA SPEC //
4
+ // // // // // // // // // // // // // // // // // // // // // // // //
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;
7
+ /**
8
+ * @public Thrown when a value fails validation, carrying every issue found.
9
+ * @since 0.1.0
10
+ * @version 1
11
+ */
12
+ class ValidationError extends Error {
13
+ issues;
14
+ constructor(issues) {
15
+ super('Failed validation:\n' + issues.map(issue => ` - ${formatPath(issue.path)}${issue.message}`).join('\n'));
16
+ this.issues = issues;
17
+ this.name = 'ValidationError';
18
+ }
19
+ }
20
+ exports.ValidationError = ValidationError;
21
+ /**
22
+ * @public Use any standard schema to parse a value.
23
+ * @since 0.1.0
24
+ * @version 1
25
+ */
26
+ const parse = (schema, value) => {
27
+ const parsed = schema['~standard'].validate(value);
28
+ if (parsed instanceof Promise) {
29
+ throw new Error('async standard schema validators are not supported');
30
+ }
31
+ return parsed;
32
+ };
33
+ exports.parse = parse;
34
+ /**
35
+ * @public Like {@link parse}, but returns the value or throws a
36
+ * {@link ValidationError}.
37
+ * @since 0.1.0
38
+ * @version 1
39
+ */
40
+ const parseOrThrow = (schema, value) => {
41
+ const parsed = (0, exports.parse)(schema, value);
42
+ if (parsed.issues)
43
+ throw new ValidationError(parsed.issues);
44
+ return parsed.value;
45
+ };
46
+ exports.parseOrThrow = parseOrThrow;
47
+ // // // // // // // // // // // // // // // // // // // // // // // //
48
+ // UTILS //
49
+ // // // // // // // // // // // // // // // // // // // // // // // //
50
+ const formatPath = (path) => path && path.length > 0
51
+ ? path.map(segment => String(typeof segment === 'object' ? segment.key : segment)).join('.') + ': '
52
+ : '';
53
+ const prependPath = (path) => (issue) => ({
54
+ ...issue,
55
+ path: [path, ...(issue.path ?? [])],
56
+ });
57
+ const define = (validate) => ({
58
+ '~standard': { version: 1, vendor: 'exto', validate },
59
+ });
60
+ const fail = (message) => ({ issues: [{ message }] });
61
+ // // // // // // // // // // // // // // // // // // // // // // // //
62
+ // BUILT-IN VALIDATORS //
63
+ // // // // // // // // // // // // // // // // // // // // // // // //
64
+ /**
65
+ * @public Defines an array of a given standard schema.
66
+ * @since 0.1.0
67
+ * @version 1
68
+ */
69
+ const array = (schema) => define((input) => {
70
+ if (!Array.isArray(input))
71
+ return fail('must be an array');
72
+ const issues = [];
73
+ const value = [];
74
+ input.forEach((item, i) => {
75
+ const parsed = (0, exports.parse)(schema, item);
76
+ if (parsed.issues)
77
+ issues.push(...parsed.issues.map(prependPath(i)));
78
+ else
79
+ value.push(parsed.value);
80
+ });
81
+ return issues.length > 0
82
+ ? { issues }
83
+ : { value };
84
+ });
85
+ exports.array = array;
86
+ /**
87
+ * @public Defines a standard schema for bigints.
88
+ * @since 0.1.0
89
+ * @version 1
90
+ */
91
+ const bigint = () => define((input) => typeof input === 'bigint'
92
+ ? { value: input }
93
+ : fail('must be a bigint'));
94
+ exports.bigint = bigint;
95
+ /**
96
+ * @public Defines a standard schema for boolean values.
97
+ * @since 0.1.0
98
+ * @version 1
99
+ */
100
+ const boolean = () => define((input) => typeof input === 'boolean'
101
+ ? { value: input }
102
+ : fail('must be a boolean'));
103
+ exports.boolean = boolean;
104
+ /**
105
+ * @public Defines a standard schema for dates, coercing ISO 8601 strings
106
+ * and epoch timestamps (in milliseconds) into `Date`.
107
+ * @since 0.1.0
108
+ * @version 1
109
+ */
110
+ const date = () => define((input) => {
111
+ const value = input instanceof Date ? input
112
+ : typeof input === 'string' || typeof input === 'number' ? new Date(input)
113
+ : undefined;
114
+ return value && !Number.isNaN(value.getTime())
115
+ ? { value }
116
+ : fail('must be a valid date, ISO 8601 string or epoch timestamp in milliseconds');
117
+ });
118
+ exports.date = date;
119
+ /**
120
+ * @public Defines a standard schema for one of the given string values.
121
+ * @since 0.1.0
122
+ * @version 1
123
+ */
124
+ const enumOf = (values) => define((input) => values.includes(input)
125
+ ? { value: input }
126
+ : fail(`must be one of: ${values.join(', ')}`));
127
+ exports.enumOf = enumOf;
128
+ /**
129
+ * @public Defines a standard schema for instances of a given class.
130
+ * @since 0.1.0
131
+ * @version 1
132
+ */
133
+ const instanceOf = (ctor) => define((input) => input instanceof ctor
134
+ ? { value: input }
135
+ : fail(`must be an instance of ${ctor.name}`));
136
+ exports.instanceOf = instanceOf;
137
+ /**
138
+ * @public Defines a standard schema for integers, within the safe integer
139
+ * range by default.
140
+ * @since 0.1.0
141
+ * @version 1
142
+ */
143
+ const integer = ({ min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = {}) => define((input) => {
144
+ if (!Number.isInteger(input))
145
+ return fail('must be an integer');
146
+ if (input < min)
147
+ return fail(`must be greater than or equal to ${min}`);
148
+ if (input > max)
149
+ return fail(`must be less than or equal to ${max}`);
150
+ return { value: input };
151
+ });
152
+ exports.integer = integer;
153
+ /**
154
+ * @public Defines a standard schema for calendar dates as `YYYY-MM-DD`
155
+ * strings.
156
+ * @since 0.1.0
157
+ * @version 1
158
+ */
159
+ const isoDate = () => define((input) => {
160
+ const valid = typeof input === 'string'
161
+ && /^\d{4}-\d{2}-\d{2}$/.test(input)
162
+ && new Date(`${input}T00:00:00Z`).toISOString().startsWith(input);
163
+ return valid
164
+ ? { value: input }
165
+ : fail('must be a valid date in the YYYY-MM-DD format');
166
+ });
167
+ exports.isoDate = isoDate;
168
+ /**
169
+ * @public Makes a given standard schema accept `null`.
170
+ * @since 0.1.0
171
+ * @version 1
172
+ */
173
+ const nullable = (schema) => define((input) => input === null
174
+ ? { value: null }
175
+ : (0, exports.parse)(schema, input));
176
+ exports.nullable = nullable;
177
+ /**
178
+ * @public Defines a standard schema for finite numbers.
179
+ * @since 0.1.0
180
+ * @version 1
181
+ */
182
+ const number = () => define((input) => Number.isFinite(input)
183
+ ? { value: input }
184
+ : fail('must be a finite number'));
185
+ exports.number = number;
186
+ /**
187
+ * @public Makes a given standard schema accept `undefined`.
188
+ * @since 0.1.0
189
+ * @version 1
190
+ */
191
+ const optional = (schema) => define((input) => input === undefined
192
+ ? { value: undefined }
193
+ : (0, exports.parse)(schema, input));
194
+ exports.optional = optional;
195
+ /**
196
+ * @public Defines a standard schema for strings, optionally limited in
197
+ * length.
198
+ * @since 0.1.0
199
+ * @version 1
200
+ */
201
+ const string = ({ max } = {}) => define((input) => {
202
+ if (typeof input !== 'string')
203
+ return fail('must be a string');
204
+ if (max !== undefined && input.length > max)
205
+ return fail(`must be at most ${max} characters long`);
206
+ return { value: input };
207
+ });
208
+ exports.string = string;
209
+ const strictObject = (shape) => define((input) => {
210
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
211
+ return fail('must be an object');
212
+ const record = input;
213
+ const issues = Object
214
+ .keys(record)
215
+ .filter(key => !(key in shape))
216
+ .map(key => ({ path: [key], message: 'unknown field' }));
217
+ const value = {};
218
+ for (const [key, schema] of Object.entries(shape)) {
219
+ const parsed = (0, exports.parse)(schema, record[key]);
220
+ if (parsed.issues && record[key] === undefined)
221
+ issues.push({ path: [key], message: 'is required' });
222
+ else if (parsed.issues)
223
+ issues.push(...parsed.issues.map(prependPath(key)));
224
+ else if (parsed.value !== undefined)
225
+ value[key] = parsed.value;
226
+ }
227
+ return issues.length > 0
228
+ ? { issues }
229
+ : { value: value };
230
+ });
231
+ exports.strictObject = strictObject;
232
+ /**
233
+ * @public Defines a standard schema for UUID strings (any version).
234
+ * @since 0.1.0
235
+ * @version 1
236
+ */
237
+ const uuid = () => define((input) => typeof input === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input)
238
+ ? { value: input }
239
+ : fail('must be a valid UUID'));
240
+ exports.uuid = uuid;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrap = exports.snakeCase = exports.camelCase = void 0;
4
+ exports.raise = raise;
5
+ const camelCase = (str) => str.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
6
+ exports.camelCase = camelCase;
7
+ const snakeCase = (str) => str.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`);
8
+ exports.snakeCase = snakeCase;
9
+ const wrap = (value) => Array.isArray(value)
10
+ ? value
11
+ : [value];
12
+ exports.wrap = wrap;
13
+ function raise(ctorOrMessage, messageOrCause, cause) {
14
+ if (typeof ctorOrMessage === 'string')
15
+ throw new Error(ctorOrMessage, { cause: messageOrCause });
16
+ throw new ctorOrMessage(messageOrCause, { cause });
17
+ }
@@ -0,0 +1,71 @@
1
+ import { SQL } from 'bun';
2
+ import { raise } from './utils';
3
+ const TYPES = {
4
+ BIGINT: () => 'BIGINT',
5
+ BINARY: () => 'BYTEA',
6
+ BOOLEAN: () => 'BOOLEAN',
7
+ CITEXT: () => 'CITEXT',
8
+ 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',
13
+ FLOAT: () => 'DOUBLE PRECISION',
14
+ INTEGER: () => 'BIGINT',
15
+ JSON: () => 'JSONB',
16
+ SEQUENCE: () => 'BIGINT',
17
+ STRING: ({ length = 255 }) => `VARCHAR(${length})`,
18
+ TEXT: () => 'TEXT',
19
+ UUID: () => 'UUID',
20
+ };
21
+ const identity = () => ({
22
+ encode: (value) => value,
23
+ decode: (value) => value,
24
+ });
25
+ const dialect = {
26
+ name: 'postgres',
27
+ placeholder: (index) => `$${index}`,
28
+ type: (col) => TYPES[col.type](col),
29
+ generated: (col) => col.type === 'SEQUENCE'
30
+ ? 'GENERATED ALWAYS AS IDENTITY'
31
+ : undefined,
32
+ codecs: {
33
+ BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
34
+ BINARY: identity(),
35
+ BOOLEAN: identity(),
36
+ CITEXT: identity(),
37
+ 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) },
42
+ JSON: identity(),
43
+ SEQUENCE: { encode: (value) => value, decode: (value) => Number(value) },
44
+ STRING: identity(),
45
+ TEXT: identity(),
46
+ UUID: identity(),
47
+ },
48
+ };
49
+ class BunPostgres {
50
+ sql;
51
+ loggers;
52
+ inTransaction;
53
+ dialect = dialect;
54
+ constructor(sql, loggers = [], inTransaction = false) {
55
+ this.sql = sql;
56
+ this.loggers = loggers;
57
+ this.inTransaction = inTransaction;
58
+ }
59
+ async execute(sql, params) {
60
+ return await this.sql.unsafe(sql, params);
61
+ }
62
+ async transaction(fn) {
63
+ const callback = (tx) => fn(new BunPostgres(tx, this.loggers, true));
64
+ const result = this.inTransaction
65
+ ? this.sql.savepoint(callback)
66
+ : this.sql.begin(callback);
67
+ return await result;
68
+ }
69
+ }
70
+ const connect = (url, { loggers = [] } = {}) => new BunPostgres(new SQL(url), loggers);
71
+ export { connect, dialect, };
@@ -0,0 +1,90 @@
1
+ import { Database } from 'bun:sqlite';
2
+ const TYPES = {
3
+ BIGINT: 'INTEGER',
4
+ BINARY: 'BLOB',
5
+ BOOLEAN: 'INTEGER',
6
+ CITEXT: 'TEXT COLLATE NOCASE',
7
+ DATE: 'TEXT',
8
+ DATETIME: 'INTEGER',
9
+ DECIMAL: 'NUMERIC',
10
+ FLOAT: 'REAL',
11
+ INTEGER: 'INTEGER',
12
+ JSON: 'TEXT',
13
+ SEQUENCE: 'INTEGER',
14
+ STRING: 'TEXT',
15
+ TEXT: 'TEXT',
16
+ UUID: 'TEXT',
17
+ };
18
+ const identity = () => ({
19
+ encode: (value) => value,
20
+ decode: (value) => value,
21
+ });
22
+ // integers are read as bigints (see `execute`), so those that aren't BIGINT
23
+ // columns go back to being numbers
24
+ const numeric = {
25
+ encode: (value) => value,
26
+ decode: (value) => Number(value),
27
+ };
28
+ const dialect = {
29
+ name: 'sqlite',
30
+ placeholder: () => '?',
31
+ type: (col) => TYPES[col.type],
32
+ generated: (col) => {
33
+ if (col.type === 'SEQUENCE' && col.primaryKey)
34
+ return 'AUTOINCREMENT';
35
+ if (col.type === 'SEQUENCE')
36
+ throw new Error(`SQLite can only generate sequences for primary keys, column "${col.name}" isn't one`);
37
+ return undefined;
38
+ },
39
+ codecs: {
40
+ BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
41
+ BINARY: identity(),
42
+ BOOLEAN: { encode: (value) => (value ? 1 : 0), decode: (value) => Number(value) === 1 },
43
+ CITEXT: identity(),
44
+ DATE: identity(),
45
+ DATETIME: { encode: (value) => value.getTime(), decode: (value) => new Date(Number(value)) },
46
+ DECIMAL: numeric,
47
+ FLOAT: numeric,
48
+ INTEGER: numeric,
49
+ JSON: { encode: (value) => JSON.stringify(value), decode: (value) => JSON.parse(value) },
50
+ SEQUENCE: numeric,
51
+ STRING: identity(),
52
+ TEXT: identity(),
53
+ UUID: identity(),
54
+ },
55
+ };
56
+ class BunSQLite {
57
+ conn;
58
+ loggers;
59
+ depth;
60
+ dialect = dialect;
61
+ constructor(conn, loggers = [], depth = 0) {
62
+ this.conn = conn;
63
+ this.loggers = loggers;
64
+ this.depth = depth;
65
+ }
66
+ async execute(sql, params) {
67
+ // without safe integers, bun:sqlite rounds anything beyond 2^53
68
+ return this.conn.prepare(sql).safeIntegers(true).all(...params);
69
+ }
70
+ // bun:sqlite's own `transaction()` is synchronous and would commit before an
71
+ // async callback settles, hence the manual BEGIN/COMMIT (SAVEPOINT if nested)
72
+ async transaction(fn) {
73
+ const savepoint = `"sp_${this.depth}"`;
74
+ const [begin, commit, rollback] = this.depth === 0
75
+ ? ['BEGIN', 'COMMIT', 'ROLLBACK']
76
+ : [`SAVEPOINT ${savepoint}`, `RELEASE ${savepoint}`, `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`];
77
+ this.conn.run(begin);
78
+ try {
79
+ const result = await fn(new BunSQLite(this.conn, this.loggers, this.depth + 1));
80
+ this.conn.run(commit);
81
+ return result;
82
+ }
83
+ catch (error) {
84
+ this.conn.run(rollback);
85
+ throw error;
86
+ }
87
+ }
88
+ }
89
+ const connect = (filename = ':memory:', { loggers = [] } = {}) => new BunSQLite(new Database(filename), loggers);
90
+ export { connect, dialect, };