@rwillians/exto 0.1.1 → 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.
@@ -0,0 +1,237 @@
1
+ // // // // // // // // // // // // // // // // // // // // // // // //
2
+ // STANDARD SCHEMA SPEC //
3
+ // // // // // // // // // // // // // // // // // // // // // // // //
4
+ // // // // // // // // // // // // // // // // // // // // // // // //
5
+ // STANDARD SCHEMA API //
6
+ // // // // // // // // // // // // // // // // // // // // // // // //
7
+ /**
8
+ * @public The Standard Schema interface.
9
+ * @since 0.1.0
10
+ * @version 1
11
+ */
12
+ export {};
13
+ /**
14
+ * @public Thrown when a value fails validation, carrying every issue found.
15
+ * @since 0.1.0
16
+ * @version 1
17
+ */
18
+ export class ValidationError extends Error {
19
+ issues;
20
+ constructor(issues) {
21
+ super('Failed validation:\n' + issues.map(issue => ` - ${formatPath(issue.path)}${issue.message}`).join('\n'));
22
+ this.issues = issues;
23
+ this.name = 'ValidationError';
24
+ }
25
+ }
26
+ /**
27
+ * @public Use any standard schema to parse a value.
28
+ * @since 0.1.0
29
+ * @version 1
30
+ */
31
+ export const parse = (schema, value) => {
32
+ const parsed = schema['~standard'].validate(value);
33
+ if (parsed instanceof Promise) {
34
+ throw new Error('async standard schema validators are not supported');
35
+ }
36
+ return parsed;
37
+ };
38
+ /**
39
+ * @public Like {@link parse}, but returns the value or throws a
40
+ * {@link ValidationError}.
41
+ * @since 0.1.0
42
+ * @version 1
43
+ */
44
+ export const parseOrThrow = (schema, value) => {
45
+ const parsed = parse(schema, value);
46
+ if (parsed.issues)
47
+ throw new ValidationError(parsed.issues);
48
+ return parsed.value;
49
+ };
50
+ // // // // // // // // // // // // // // // // // // // // // // // //
51
+ // UTILS //
52
+ // // // // // // // // // // // // // // // // // // // // // // // //
53
+ const formatPath = (path) => path && path.length > 0
54
+ ? path.map(segment => String(typeof segment === 'object' ? segment.key : segment)).join('.') + ': '
55
+ : '';
56
+ const prependPath = (path) => (issue) => ({
57
+ ...issue,
58
+ path: [path, ...(issue.path ?? [])],
59
+ });
60
+ const define = (validate) => ({
61
+ '~standard': { version: 1, vendor: 'exto', validate },
62
+ });
63
+ const fail = (message) => ({ issues: [{ message }] });
64
+ // // // // // // // // // // // // // // // // // // // // // // // //
65
+ // BUILT-IN VALIDATORS //
66
+ // // // // // // // // // // // // // // // // // // // // // // // //
67
+ /**
68
+ * @public Defines an array of a given standard schema.
69
+ * @since 0.1.0
70
+ * @version 1
71
+ */
72
+ export const array = (schema) => define((input) => {
73
+ if (!Array.isArray(input))
74
+ return fail('must be an array');
75
+ const issues = [];
76
+ const value = [];
77
+ input.forEach((item, i) => {
78
+ const parsed = parse(schema, item);
79
+ if (parsed.issues)
80
+ issues.push(...parsed.issues.map(prependPath(i)));
81
+ else
82
+ value.push(parsed.value);
83
+ });
84
+ return issues.length > 0
85
+ ? { issues }
86
+ : { value };
87
+ });
88
+ /**
89
+ * @public Defines a standard schema for bigints.
90
+ * @since 0.1.0
91
+ * @version 1
92
+ */
93
+ export const bigint = () => define((input) => typeof input === 'bigint'
94
+ ? { value: input }
95
+ : fail('must be a bigint'));
96
+ /**
97
+ * @public Defines a standard schema for boolean values.
98
+ * @since 0.1.0
99
+ * @version 1
100
+ */
101
+ export const boolean = () => define((input) => typeof input === 'boolean'
102
+ ? { value: input }
103
+ : fail('must be a 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
+ export 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
+ /**
119
+ * @public Defines a standard schema for one of the given string values.
120
+ * @since 0.1.0
121
+ * @version 1
122
+ */
123
+ export const enumOf = (values) => define((input) => values.includes(input)
124
+ ? { value: input }
125
+ : fail(`must be one of: ${values.join(', ')}`));
126
+ /**
127
+ * @public Defines a standard schema for instances of a given class.
128
+ * @since 0.1.0
129
+ * @version 1
130
+ */
131
+ export const instanceOf = (ctor) => define((input) => input instanceof ctor
132
+ ? { value: input }
133
+ : fail(`must be an instance of ${ctor.name}`));
134
+ /**
135
+ * @public Defines a standard schema for integers, within the safe integer
136
+ * range by default.
137
+ * @since 0.1.0
138
+ * @version 1
139
+ */
140
+ export const integer = ({ min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = {}) => define((input) => {
141
+ if (!Number.isInteger(input))
142
+ return fail('must be an integer');
143
+ if (input < min)
144
+ return fail(`must be greater than or equal to ${min}`);
145
+ if (input > max)
146
+ return fail(`must be less than or equal to ${max}`);
147
+ return { value: input };
148
+ });
149
+ /**
150
+ * @public Defines a standard schema for calendar dates as `YYYY-MM-DD`
151
+ * strings.
152
+ * @since 0.1.0
153
+ * @version 1
154
+ */
155
+ export const isoDate = () => define((input) => {
156
+ const valid = typeof input === 'string'
157
+ && /^\d{4}-\d{2}-\d{2}$/.test(input)
158
+ && new Date(`${input}T00:00:00Z`).toISOString().startsWith(input);
159
+ return valid
160
+ ? { value: input }
161
+ : fail('must be a valid date in the YYYY-MM-DD format');
162
+ });
163
+ /**
164
+ * @public Makes a given standard schema accept `null`.
165
+ * @since 0.1.0
166
+ * @version 1
167
+ */
168
+ export const nullable = (schema) => define((input) => input === null
169
+ ? { value: null }
170
+ : parse(schema, input));
171
+ /**
172
+ * @public Defines a standard schema for finite numbers.
173
+ * @since 0.1.0
174
+ * @version 1
175
+ */
176
+ export const number = () => define((input) => Number.isFinite(input)
177
+ ? { value: input }
178
+ : fail('must be a finite number'));
179
+ /**
180
+ * @public Makes a given standard schema accept `undefined`.
181
+ * @since 0.1.0
182
+ * @version 1
183
+ */
184
+ export const optional = (schema) => define((input) => input === undefined
185
+ ? { value: undefined }
186
+ : parse(schema, input));
187
+ /**
188
+ * @public Defines a standard schema for plain objects with string keys.
189
+ * @since 0.2.0
190
+ * @version 1
191
+ */
192
+ export const record = () => define((input) => typeof input === 'object' && input !== null && Object.getPrototypeOf(input) === Object.prototype
193
+ ? { value: input }
194
+ : fail('must be a plain object'));
195
+ /**
196
+ * @public Defines a standard schema for strings, optionally limited in
197
+ * length.
198
+ * @since 0.1.0
199
+ * @version 1
200
+ */
201
+ export 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
+ export const strictObject = (shape) => define((input) => {
209
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
210
+ return fail('must be an object');
211
+ const record = input;
212
+ const issues = Object
213
+ .keys(record)
214
+ .filter(key => !(key in shape))
215
+ .map(key => ({ path: [key], message: 'unknown field' }));
216
+ const value = {};
217
+ for (const [key, schema] of Object.entries(shape)) {
218
+ const parsed = parse(schema, record[key]);
219
+ if (parsed.issues && record[key] === undefined)
220
+ issues.push({ path: [key], message: 'is required' });
221
+ else if (parsed.issues)
222
+ issues.push(...parsed.issues.map(prependPath(key)));
223
+ else if (parsed.value !== undefined)
224
+ value[key] = parsed.value;
225
+ }
226
+ return issues.length > 0
227
+ ? { issues }
228
+ : { value: value };
229
+ });
230
+ /**
231
+ * @public Defines a standard schema for UUID strings (any version).
232
+ * @since 0.1.0
233
+ * @version 1
234
+ */
235
+ export 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)
236
+ ? { value: input }
237
+ : fail('must be a valid UUID'));
@@ -0,0 +1,10 @@
1
+ export const camelCase = (str) => str.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
2
+ export const snakeCase = (str) => str.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`);
3
+ export const wrap = (value) => Array.isArray(value)
4
+ ? value
5
+ : [value];
6
+ export function raise(ctorOrMessage, messageOrCause, cause) {
7
+ if (typeof ctorOrMessage === 'string')
8
+ throw new Error(ctorOrMessage, { cause: messageOrCause });
9
+ throw new ctorOrMessage(messageOrCause, { cause });
10
+ }
@@ -0,0 +1,17 @@
1
+ import { SQL } from 'bun';
2
+ import type { Dialect, IDatabase, ILogger, Row } from './index';
3
+ declare const dialect: Dialect;
4
+ declare class BunPostgres implements IDatabase {
5
+ private readonly sql;
6
+ readonly loggers: ILogger[];
7
+ private readonly inTransaction;
8
+ readonly dialect: Dialect;
9
+ constructor(sql: SQL, loggers?: ILogger[], inTransaction?: boolean);
10
+ execute(sql: string, params: unknown[]): Promise<Row[] & SQL.ResultMetadata>;
11
+ close(): Promise<void>;
12
+ transaction<T>(fn: (tx: IDatabase) => Promise<T>): Promise<T>;
13
+ }
14
+ declare const connect: (url: string, { loggers }?: {
15
+ loggers?: ILogger[];
16
+ }) => BunPostgres;
17
+ export { connect, dialect, };
@@ -0,0 +1,17 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type { Dialect, IDatabase, ILogger, Row } from './index';
3
+ declare const dialect: Dialect;
4
+ declare class BunSQLite implements IDatabase {
5
+ private readonly conn;
6
+ readonly loggers: ILogger[];
7
+ private readonly depth;
8
+ readonly dialect: Dialect;
9
+ constructor(conn: Database, loggers?: ILogger[], depth?: number);
10
+ execute(sql: string, params: unknown[]): Promise<Row[]>;
11
+ close(): Promise<void>;
12
+ transaction<T>(fn: (tx: IDatabase) => Promise<T>): Promise<T>;
13
+ }
14
+ declare const connect: (filename?: string, { loggers }?: {
15
+ loggers?: ILogger[];
16
+ }) => BunSQLite;
17
+ export { connect, dialect, };