@prisma-next/psl-printer 0.5.0-dev.8 → 0.5.0-dev.80

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.
@@ -1,98 +0,0 @@
1
- import type { ColumnDefault } from '@prisma-next/contract/types';
2
-
3
- /**
4
- * Parses a raw database default expression into a normalized ColumnDefault.
5
- * When nativeType is provided, parsing can preserve Postgres semantics for
6
- * timestamp and JSON defaults that would otherwise be ambiguous.
7
- */
8
-
9
- const NEXTVAL_PATTERN = /^nextval\s*\(/i;
10
- const NOW_FUNCTION_PATTERN = /^(now\s*\(\s*\)|CURRENT_TIMESTAMP)$/i;
11
- const CLOCK_TIMESTAMP_PATTERN = /^clock_timestamp\s*\(\s*\)$/i;
12
- const TIMESTAMP_CAST_SUFFIX = /::timestamp(?:tz|\s+(?:with|without)\s+time\s+zone)?$/i;
13
- const TEXT_CAST_SUFFIX = /::text$/i;
14
- const NOW_LITERAL_PATTERN = /^'now'$/i;
15
- const UUID_PATTERN = /^gen_random_uuid\s*\(\s*\)$/i;
16
- const UUID_OSSP_PATTERN = /^uuid_generate_v4\s*\(\s*\)$/i;
17
- const NULL_PATTERN = /^NULL(?:::.+)?$/i;
18
- const TRUE_PATTERN = /^true$/i;
19
- const FALSE_PATTERN = /^false$/i;
20
- const NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
21
- const JSON_CAST_SUFFIX = /::jsonb?$/i;
22
- const STRING_LITERAL_PATTERN = /^'((?:[^']|'')*)'(?:::(?:"[^"]+"|[\w\s]+)(?:\(\d+\))?)?$/;
23
-
24
- function canonicalizeTimestampDefault(expr: string): string | undefined {
25
- if (NOW_FUNCTION_PATTERN.test(expr)) return 'now()';
26
- if (CLOCK_TIMESTAMP_PATTERN.test(expr)) return 'clock_timestamp()';
27
-
28
- if (!TIMESTAMP_CAST_SUFFIX.test(expr)) return undefined;
29
-
30
- let inner = expr.replace(TIMESTAMP_CAST_SUFFIX, '').trim();
31
- if (inner.startsWith('(') && inner.endsWith(')')) {
32
- inner = inner.slice(1, -1).trim();
33
- }
34
-
35
- if (NOW_FUNCTION_PATTERN.test(inner)) return 'now()';
36
- if (CLOCK_TIMESTAMP_PATTERN.test(inner)) return 'clock_timestamp()';
37
-
38
- inner = inner.replace(TEXT_CAST_SUFFIX, '').trim();
39
- if (NOW_LITERAL_PATTERN.test(inner)) return 'now()';
40
-
41
- return undefined;
42
- }
43
-
44
- export function parseRawDefault(
45
- rawDefault: string,
46
- nativeType?: string,
47
- ): ColumnDefault | undefined {
48
- const trimmed = rawDefault.trim();
49
- const normalizedType = nativeType?.toLowerCase();
50
-
51
- if (NEXTVAL_PATTERN.test(trimmed)) {
52
- return { kind: 'function', expression: 'autoincrement()' };
53
- }
54
-
55
- const canonicalTimestamp = canonicalizeTimestampDefault(trimmed);
56
- if (canonicalTimestamp) {
57
- return { kind: 'function', expression: canonicalTimestamp };
58
- }
59
-
60
- if (UUID_PATTERN.test(trimmed) || UUID_OSSP_PATTERN.test(trimmed)) {
61
- return { kind: 'function', expression: 'gen_random_uuid()' };
62
- }
63
-
64
- if (NULL_PATTERN.test(trimmed)) {
65
- return { kind: 'literal', value: null };
66
- }
67
-
68
- if (TRUE_PATTERN.test(trimmed)) {
69
- return { kind: 'literal', value: true };
70
- }
71
-
72
- if (FALSE_PATTERN.test(trimmed)) {
73
- return { kind: 'literal', value: false };
74
- }
75
-
76
- if (NUMERIC_PATTERN.test(trimmed)) {
77
- return { kind: 'literal', value: Number(trimmed) };
78
- }
79
-
80
- const stringMatch = trimmed.match(STRING_LITERAL_PATTERN);
81
- if (stringMatch?.[1] !== undefined) {
82
- const unescaped = stringMatch[1].replace(/''/g, "'");
83
- if (normalizedType === 'json' || normalizedType === 'jsonb') {
84
- if (JSON_CAST_SUFFIX.test(trimmed)) {
85
- return { kind: 'function', expression: trimmed };
86
- }
87
- try {
88
- return { kind: 'literal', value: JSON.parse(unescaped) };
89
- } catch {
90
- // Fall through to the string form for malformed/non-JSON values.
91
- }
92
- }
93
- return { kind: 'literal', value: unescaped };
94
- }
95
-
96
- // Unrecognized — return as function with raw expression
97
- return { kind: 'function', expression: trimmed };
98
- }
@@ -1,239 +0,0 @@
1
- import type { SqlForeignKeyIR } from '@prisma-next/sql-schema-ir/types';
2
- import { deriveBackRelationFieldName, deriveRelationFieldName, pluralize } from './name-transforms';
3
- import type { PslPrintableSqlTable } from './schema-validation';
4
- import type { RelationField } from './types';
5
-
6
- /**
7
- * Default referential actions — when the FK uses these, we omit them from the PSL output.
8
- */
9
- const DEFAULT_ON_DELETE = 'noAction';
10
- const DEFAULT_ON_UPDATE = 'noAction';
11
-
12
- /**
13
- * Maps SqlReferentialAction to PSL-compatible casing.
14
- */
15
- const REFERENTIAL_ACTION_PSL: Record<string, string> = {
16
- noAction: 'NoAction',
17
- restrict: 'Restrict',
18
- cascade: 'Cascade',
19
- setNull: 'SetNull',
20
- setDefault: 'SetDefault',
21
- };
22
-
23
- export type InferredRelations = {
24
- /** Relation fields keyed by table name → array of fields to add */
25
- readonly relationsByTable: ReadonlyMap<string, readonly RelationField[]>;
26
- };
27
-
28
- /**
29
- * Infers relation fields from foreign keys across all tables.
30
- *
31
- * For each FK:
32
- * 1. Creates a relation field on the child table (the table with the FK)
33
- * 2. Creates a back-relation field on the parent table (the referenced table)
34
- * 3. Detects 1:1 vs 1:N cardinality
35
- * 4. Handles multiple FKs to the same parent (named relations)
36
- * 5. Handles self-referencing FKs
37
- */
38
- export function inferRelations(
39
- tables: Record<string, PslPrintableSqlTable>,
40
- modelNameMap: ReadonlyMap<string, string>,
41
- ): InferredRelations {
42
- const relationsByTable = new Map<string, RelationField[]>();
43
-
44
- // Track FK count from each child table to each parent table, for disambiguation
45
- const fkCountByPair = new Map<string, number>();
46
- for (const table of Object.values(tables)) {
47
- for (const fk of table.foreignKeys) {
48
- const pairKey = `${table.name}→${fk.referencedTable}`;
49
- fkCountByPair.set(pairKey, (fkCountByPair.get(pairKey) ?? 0) + 1);
50
- }
51
- }
52
-
53
- // Track which field names are used per table (including existing columns) for collision avoidance
54
- const usedFieldNames = new Map<string, Set<string>>();
55
- for (const table of Object.values(tables)) {
56
- const names = new Set<string>();
57
- for (const col of Object.values(table.columns)) {
58
- names.add(col.name);
59
- }
60
- usedFieldNames.set(table.name, names);
61
- }
62
-
63
- for (const table of Object.values(tables)) {
64
- for (const fk of table.foreignKeys) {
65
- const childTableName = table.name;
66
- const parentTableName = fk.referencedTable;
67
- const childUsed = usedFieldNames.get(childTableName) as Set<string>;
68
- const childModelName = modelNameMap.get(childTableName) ?? childTableName;
69
- const parentModelName = modelNameMap.get(parentTableName) ?? parentTableName;
70
- const pairKey = `${childTableName}→${parentTableName}`;
71
- const isSelfRelation = childTableName === parentTableName;
72
- const needsRelationName = (fkCountByPair.get(pairKey) as number) > 1 || isSelfRelation;
73
-
74
- // Determine cardinality
75
- const isOneToOne = detectOneToOne(fk, table);
76
-
77
- // Child table: relation field (e.g., author User @relation(...))
78
- const childRelFieldName = resolveUniqueFieldName(
79
- deriveRelationFieldName(fk.columns, parentTableName),
80
- childUsed,
81
- parentModelName,
82
- );
83
- const relationName = needsRelationName
84
- ? deriveRelationName(fk, childRelFieldName, parentModelName, isSelfRelation)
85
- : undefined;
86
- const childOptional = fk.columns.some(
87
- (columnName) => table.columns[columnName]?.nullable ?? false,
88
- );
89
-
90
- const childRelField = buildChildRelationField(
91
- childRelFieldName,
92
- parentModelName,
93
- fk,
94
- childOptional,
95
- relationName,
96
- );
97
-
98
- addRelationField(relationsByTable, childTableName, childRelField);
99
- childUsed.add(childRelFieldName);
100
-
101
- // Parent table: back-relation field (e.g., posts Post[])
102
- const parentUsed = usedFieldNames.get(parentTableName) ?? new Set();
103
- usedFieldNames.set(parentTableName, parentUsed);
104
-
105
- const backRelFieldName = resolveUniqueFieldName(
106
- deriveBackRelationFieldName(childModelName, isOneToOne),
107
- parentUsed,
108
- childModelName,
109
- );
110
-
111
- const backRelField: RelationField = {
112
- fieldName: backRelFieldName,
113
- typeName: childModelName,
114
- optional: isOneToOne,
115
- list: !isOneToOne,
116
- relationName,
117
- };
118
-
119
- addRelationField(relationsByTable, parentTableName, backRelField);
120
- parentUsed.add(backRelFieldName);
121
- }
122
- }
123
-
124
- return { relationsByTable };
125
- }
126
-
127
- /**
128
- * Detects whether a FK represents a 1:1 relationship.
129
- * A FK is 1:1 if:
130
- * - The FK columns exactly match the table's PK columns, OR
131
- * - The FK columns exactly match a unique constraint
132
- */
133
- function detectOneToOne(fk: SqlForeignKeyIR, table: PslPrintableSqlTable): boolean {
134
- const fkCols = [...fk.columns].sort();
135
-
136
- // FK columns == PK columns → 1:1
137
- if (table.primaryKey) {
138
- const pkCols = [...table.primaryKey.columns].sort();
139
- if (pkCols.length === fkCols.length && pkCols.every((c, i) => c === fkCols[i])) {
140
- return true;
141
- }
142
- }
143
-
144
- // FK columns == unique columns → 1:1
145
- for (const unique of table.uniques) {
146
- const uniqueCols = [...unique.columns].sort();
147
- if (uniqueCols.length === fkCols.length && uniqueCols.every((c, i) => c === fkCols[i])) {
148
- return true;
149
- }
150
- }
151
-
152
- return false;
153
- }
154
-
155
- /**
156
- * Derives a relation name for disambiguation.
157
- * Uses the FK constraint name if available, otherwise generates one from the relation shape.
158
- */
159
- function deriveRelationName(
160
- fk: SqlForeignKeyIR,
161
- childRelationFieldName: string,
162
- parentModelName: string,
163
- isSelfRelation: boolean,
164
- ): string {
165
- if (fk.name) {
166
- return fk.name;
167
- }
168
- if (isSelfRelation) {
169
- return `${childRelationFieldName.charAt(0).toUpperCase() + childRelationFieldName.slice(1)}${pluralize(parentModelName)}`;
170
- }
171
- return fk.columns.join('_');
172
- }
173
-
174
- /**
175
- * Builds a child-side relation field with @relation attributes.
176
- */
177
- function buildChildRelationField(
178
- fieldName: string,
179
- parentModelName: string,
180
- fk: SqlForeignKeyIR,
181
- optional: boolean,
182
- relationName?: string,
183
- ): RelationField {
184
- const onDelete = fk.onDelete && fk.onDelete !== DEFAULT_ON_DELETE ? fk.onDelete : undefined;
185
- const onUpdate = fk.onUpdate && fk.onUpdate !== DEFAULT_ON_UPDATE ? fk.onUpdate : undefined;
186
-
187
- return {
188
- fieldName,
189
- typeName: parentModelName,
190
- referencedTableName: fk.referencedTable,
191
- optional,
192
- list: false,
193
- relationName,
194
- fkName: fk.name,
195
- fields: fk.columns,
196
- references: fk.referencedColumns,
197
- onDelete: onDelete ? REFERENTIAL_ACTION_PSL[onDelete] : undefined,
198
- onUpdate: onUpdate ? REFERENTIAL_ACTION_PSL[onUpdate] : undefined,
199
- };
200
- }
201
-
202
- /**
203
- * Resolves a unique field name by appending the model name if there's a collision.
204
- */
205
- function resolveUniqueFieldName(
206
- desired: string,
207
- usedNames: ReadonlySet<string>,
208
- fallbackSuffix: string,
209
- ): string {
210
- if (!usedNames.has(desired)) {
211
- return desired;
212
- }
213
-
214
- // Try appending the model name
215
- const withSuffix = `${desired}${fallbackSuffix}`;
216
- if (!usedNames.has(withSuffix)) {
217
- return withSuffix;
218
- }
219
-
220
- // Last resort: append a number
221
- let counter = 2;
222
- while (usedNames.has(`${desired}${counter}`)) {
223
- counter++;
224
- }
225
- return `${desired}${counter}`;
226
- }
227
-
228
- function addRelationField(
229
- map: Map<string, RelationField[]>,
230
- tableName: string,
231
- field: RelationField,
232
- ): void {
233
- const existing = map.get(tableName);
234
- if (existing) {
235
- existing.push(field);
236
- } else {
237
- map.set(tableName, [field]);
238
- }
239
- }
@@ -1,308 +0,0 @@
1
- import type { ColumnDefault } from '@prisma-next/contract/types';
2
- import type {
3
- DependencyIR,
4
- PrimaryKey,
5
- SqlAnnotations,
6
- SqlForeignKeyIR,
7
- SqlIndexIR,
8
- SqlReferentialAction,
9
- SqlSchemaIR,
10
- SqlUniqueIR,
11
- } from '@prisma-next/sql-schema-ir/types';
12
-
13
- const REFERENTIAL_ACTIONS = new Set<SqlReferentialAction>([
14
- 'noAction',
15
- 'restrict',
16
- 'cascade',
17
- 'setNull',
18
- 'setDefault',
19
- ]);
20
-
21
- export type PrintableSqlColumnDefault = string | ColumnDefault;
22
-
23
- type ColumnDefaultLiteralValue = Extract<ColumnDefault, { readonly kind: 'literal' }>['value'];
24
-
25
- export type PslPrintableSqlColumn = {
26
- readonly name: string;
27
- readonly nativeType: string;
28
- readonly nullable: boolean;
29
- readonly default?: PrintableSqlColumnDefault;
30
- readonly annotations?: SqlAnnotations;
31
- };
32
-
33
- export type PslPrintableSqlTable = {
34
- readonly name: string;
35
- readonly columns: Record<string, PslPrintableSqlColumn>;
36
- readonly primaryKey?: PrimaryKey;
37
- readonly foreignKeys: readonly SqlForeignKeyIR[];
38
- readonly uniques: readonly SqlUniqueIR[];
39
- readonly indexes: readonly SqlIndexIR[];
40
- readonly annotations?: SqlAnnotations;
41
- };
42
-
43
- export type PslPrintableSqlSchemaIR = Omit<SqlSchemaIR, 'tables'> & {
44
- readonly tables: Record<string, PslPrintableSqlTable>;
45
- };
46
-
47
- export function validatePrintableSqlSchemaIR(value: unknown): PslPrintableSqlSchemaIR {
48
- const root = expectRecord(value, 'schema');
49
-
50
- return {
51
- tables: validateTables(root['tables'], 'schema.tables'),
52
- dependencies: validateDependencies(root['dependencies'], 'schema.dependencies'),
53
- ...ifDefined('annotations', validateAnnotations(root['annotations'], 'schema.annotations')),
54
- };
55
- }
56
-
57
- function validateTables(value: unknown, path: string): Record<string, PslPrintableSqlTable> {
58
- const tables = expectRecord(value, path);
59
- const validated: Record<string, PslPrintableSqlTable> = {};
60
-
61
- for (const [tableName, tableValue] of Object.entries(tables)) {
62
- const tablePath = `${path}.${tableName}`;
63
- const table = expectRecord(tableValue, tablePath);
64
-
65
- validated[tableName] = {
66
- name: expectString(table['name'], `${tablePath}.name`),
67
- columns: validateColumns(table['columns'], `${tablePath}.columns`),
68
- foreignKeys: validateForeignKeys(table['foreignKeys'], `${tablePath}.foreignKeys`),
69
- uniques: validateUniques(table['uniques'], `${tablePath}.uniques`),
70
- indexes: validateIndexes(table['indexes'], `${tablePath}.indexes`),
71
- ...ifDefined(
72
- 'primaryKey',
73
- validatePrimaryKey(table['primaryKey'], `${tablePath}.primaryKey`),
74
- ),
75
- ...ifDefined(
76
- 'annotations',
77
- validateAnnotations(table['annotations'], `${tablePath}.annotations`),
78
- ),
79
- };
80
- }
81
-
82
- return validated;
83
- }
84
-
85
- function validateColumns(value: unknown, path: string): Record<string, PslPrintableSqlColumn> {
86
- const columns = expectRecord(value, path);
87
- const validated: Record<string, PslPrintableSqlColumn> = {};
88
-
89
- for (const [columnName, columnValue] of Object.entries(columns)) {
90
- const columnPath = `${path}.${columnName}`;
91
- const column = expectRecord(columnValue, columnPath);
92
-
93
- validated[columnName] = {
94
- name: expectString(column['name'], `${columnPath}.name`),
95
- nativeType: expectString(column['nativeType'], `${columnPath}.nativeType`),
96
- nullable: expectBoolean(column['nullable'], `${columnPath}.nullable`),
97
- ...ifDefined('default', validateColumnDefault(column['default'], `${columnPath}.default`)),
98
- ...ifDefined(
99
- 'annotations',
100
- validateAnnotations(column['annotations'], `${columnPath}.annotations`),
101
- ),
102
- };
103
- }
104
-
105
- return validated;
106
- }
107
-
108
- function validatePrimaryKey(value: unknown, path: string): PrimaryKey | undefined {
109
- if (value === undefined) {
110
- return undefined;
111
- }
112
-
113
- const primaryKey = expectRecord(value, path);
114
- return {
115
- columns: validateStringArray(primaryKey['columns'], `${path}.columns`),
116
- ...ifDefined('name', validateOptionalString(primaryKey['name'], `${path}.name`)),
117
- };
118
- }
119
-
120
- function validateForeignKeys(value: unknown, path: string): readonly SqlForeignKeyIR[] {
121
- const foreignKeys = expectArray(value, path);
122
- return foreignKeys.map((foreignKey, index) => {
123
- const foreignKeyPath = `${path}[${index}]`;
124
- const record = expectRecord(foreignKey, foreignKeyPath);
125
-
126
- return {
127
- columns: validateStringArray(record['columns'], `${foreignKeyPath}.columns`),
128
- referencedTable: expectString(record['referencedTable'], `${foreignKeyPath}.referencedTable`),
129
- referencedColumns: validateStringArray(
130
- record['referencedColumns'],
131
- `${foreignKeyPath}.referencedColumns`,
132
- ),
133
- ...ifDefined('name', validateOptionalString(record['name'], `${foreignKeyPath}.name`)),
134
- ...ifDefined(
135
- 'onDelete',
136
- validateReferentialAction(record['onDelete'], `${foreignKeyPath}.onDelete`),
137
- ),
138
- ...ifDefined(
139
- 'onUpdate',
140
- validateReferentialAction(record['onUpdate'], `${foreignKeyPath}.onUpdate`),
141
- ),
142
- ...ifDefined(
143
- 'annotations',
144
- validateAnnotations(record['annotations'], `${foreignKeyPath}.annotations`),
145
- ),
146
- };
147
- });
148
- }
149
-
150
- function validateUniques(value: unknown, path: string): readonly SqlUniqueIR[] {
151
- const uniques = expectArray(value, path);
152
- return uniques.map((uniqueValue, index) => {
153
- const uniquePath = `${path}[${index}]`;
154
- const unique = expectRecord(uniqueValue, uniquePath);
155
-
156
- return {
157
- columns: validateStringArray(unique['columns'], `${uniquePath}.columns`),
158
- ...ifDefined('name', validateOptionalString(unique['name'], `${uniquePath}.name`)),
159
- ...ifDefined(
160
- 'annotations',
161
- validateAnnotations(unique['annotations'], `${uniquePath}.annotations`),
162
- ),
163
- };
164
- });
165
- }
166
-
167
- function validateIndexes(value: unknown, path: string): readonly SqlIndexIR[] {
168
- const indexes = expectArray(value, path);
169
- return indexes.map((indexValue, index) => {
170
- const indexPath = `${path}[${index}]`;
171
- const record = expectRecord(indexValue, indexPath);
172
-
173
- return {
174
- columns: validateStringArray(record['columns'], `${indexPath}.columns`),
175
- unique: expectBoolean(record['unique'], `${indexPath}.unique`),
176
- ...ifDefined('name', validateOptionalString(record['name'], `${indexPath}.name`)),
177
- ...ifDefined(
178
- 'annotations',
179
- validateAnnotations(record['annotations'], `${indexPath}.annotations`),
180
- ),
181
- };
182
- });
183
- }
184
-
185
- function validateDependencies(value: unknown, path: string): readonly DependencyIR[] {
186
- const dependencies = expectArray(value, path);
187
- return dependencies.map((dependencyValue, index) => {
188
- const dependencyPath = `${path}[${index}]`;
189
- const dependency = expectRecord(dependencyValue, dependencyPath);
190
-
191
- return {
192
- id: expectString(dependency['id'], `${dependencyPath}.id`),
193
- };
194
- });
195
- }
196
-
197
- function validateAnnotations(value: unknown, path: string): SqlAnnotations | undefined {
198
- if (value === undefined) {
199
- return undefined;
200
- }
201
-
202
- return expectRecord(value, path);
203
- }
204
-
205
- function validateColumnDefault(
206
- value: unknown,
207
- path: string,
208
- ): PrintableSqlColumnDefault | undefined {
209
- if (value === undefined) {
210
- return undefined;
211
- }
212
-
213
- if (typeof value === 'string') {
214
- return value;
215
- }
216
-
217
- const columnDefault = expectRecord(value, path);
218
- const kind = expectString(columnDefault['kind'], `${path}.kind`);
219
-
220
- if (kind === 'literal') {
221
- if (!Object.hasOwn(columnDefault, 'value')) {
222
- throw new Error(`${path}.value must be present for literal defaults`);
223
- }
224
- return {
225
- kind: 'literal',
226
- value: columnDefault['value'] as ColumnDefaultLiteralValue,
227
- };
228
- }
229
-
230
- if (kind === 'function') {
231
- return {
232
- kind: 'function',
233
- expression: expectString(columnDefault['expression'], `${path}.expression`),
234
- };
235
- }
236
-
237
- throw new Error(`${path}.kind must be "literal" or "function"`);
238
- }
239
-
240
- function validateReferentialAction(value: unknown, path: string): SqlReferentialAction | undefined {
241
- if (value === undefined) {
242
- return undefined;
243
- }
244
-
245
- const action = expectString(value, path) as SqlReferentialAction;
246
- if (!REFERENTIAL_ACTIONS.has(action)) {
247
- throw new Error(
248
- `${path} must be one of ${[...REFERENTIAL_ACTIONS].map((item) => `"${item}"`).join(', ')}`,
249
- );
250
- }
251
- return action;
252
- }
253
-
254
- function validateStringArray(value: unknown, path: string): readonly string[] {
255
- const items = expectArray(value, path);
256
- return items.map((item, index) => expectString(item, `${path}[${index}]`));
257
- }
258
-
259
- function validateOptionalString(value: unknown, path: string): string | undefined {
260
- if (value === undefined) {
261
- return undefined;
262
- }
263
-
264
- return expectString(value, path);
265
- }
266
-
267
- function expectRecord(value: unknown, path: string): Record<string, unknown> {
268
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
269
- throw new Error(`${path} must be an object`);
270
- }
271
-
272
- return value as Record<string, unknown>;
273
- }
274
-
275
- function expectArray(value: unknown, path: string): readonly unknown[] {
276
- if (!Array.isArray(value)) {
277
- throw new Error(`${path} must be an array`);
278
- }
279
-
280
- return value;
281
- }
282
-
283
- function expectString(value: unknown, path: string): string {
284
- if (typeof value !== 'string') {
285
- throw new Error(`${path} must be a string`);
286
- }
287
-
288
- return value;
289
- }
290
-
291
- function expectBoolean(value: unknown, path: string): boolean {
292
- if (typeof value !== 'boolean') {
293
- throw new Error(`${path} must be a boolean`);
294
- }
295
-
296
- return value;
297
- }
298
-
299
- function ifDefined<TKey extends string, TValue>(
300
- key: TKey,
301
- value: TValue | undefined,
302
- ): { readonly [K in TKey]: TValue } | Record<string, never> {
303
- if (value === undefined) {
304
- return {};
305
- }
306
-
307
- return { [key]: value } as { readonly [K in TKey]: TValue };
308
- }