metal-orm 1.0.91 → 1.0.92

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.91",
3
+ "version": "1.0.92",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -4,6 +4,14 @@ import { buildSchemaMetadata } from './schema.mjs';
4
4
 
5
5
  const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
6
6
 
7
+ const sanitizePropertyName = columnName => {
8
+ if (!columnName) return '';
9
+ return columnName
10
+ .replace(/\s+/g, '_')
11
+ .replace(/[^a-zA-Z0-9_$]/g, '_')
12
+ .replace(/^[0-9]/, '_$&');
13
+ };
14
+
7
15
  const formatJsDoc = comment => {
8
16
  if (!comment) return null;
9
17
  const normalized = comment.replace(/\r\n?/g, '\n').trim();
@@ -159,7 +167,7 @@ const buildColumnDoc = column => {
159
167
  return entries.join('\n');
160
168
  };
161
169
 
162
- const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema) => {
170
+ const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema, propertyName) => {
163
171
  const base = parseColumnType(column.type);
164
172
  let expr = base.factory;
165
173
 
@@ -198,6 +206,10 @@ const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema) =>
198
206
  const tsType = base.ts || 'any';
199
207
  const optional = !column.notNull;
200
208
 
209
+ if (column.name !== propertyName) {
210
+ expr = `{ ...${expr}, name: '${escapeJsString(column.name)}' }`;
211
+ }
212
+
201
213
  return {
202
214
  decorator,
203
215
  expr,
@@ -236,10 +248,11 @@ const renderEntityClassLines = ({ table, className, naming, relations, resolveCl
236
248
  lines.push(`export class ${className} {`);
237
249
 
238
250
  for (const col of table.columns) {
239
- const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema);
251
+ const propertyName = sanitizePropertyName(col.name);
252
+ const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema, propertyName);
240
253
  appendJsDoc(lines, rendered.comment, ' ');
241
254
  lines.push(` @${rendered.decorator}(${rendered.expr})`);
242
- lines.push(` ${col.name}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
255
+ lines.push(` ${propertyName}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
243
256
  lines.push('');
244
257
  }
245
258
 
@@ -12,6 +12,7 @@ export interface ColumnOptions {
12
12
  notNull?: boolean;
13
13
  primary?: boolean;
14
14
  tsType?: ColumnDef['tsType'];
15
+ name?: string;
15
16
  }
16
17
 
17
18
  /**
@@ -35,7 +36,8 @@ const normalizeColumnInput = (input: ColumnInput): ColumnDefLike => {
35
36
  generated: asDefinition.generated,
36
37
  check: asDefinition.check,
37
38
  references: asDefinition.references,
38
- comment: asDefinition.comment
39
+ comment: asDefinition.comment,
40
+ name: asOptions.name ?? asDefinition.name
39
41
  };
40
42
 
41
43
  if (!column.type) {
@@ -1,23 +1,26 @@
1
1
  import type { ColumnDef } from '../../../schema/column-types.js';
2
- import type { OpenApiSchema } from '../types.js';
2
+ import type { OpenApiSchema, OpenApiDialect } from '../types.js';
3
3
  import { columnTypeToOpenApiType, columnTypeToOpenApiFormat } from '../type-mappings.js';
4
+ import { applyNullability, isNullableColumn } from '../utilities.js';
4
5
 
5
- export function columnToOpenApiSchema(col: ColumnDef): OpenApiSchema {
6
+ export function columnToOpenApiSchema(
7
+ col: ColumnDef,
8
+ dialect: OpenApiDialect = 'openapi-3.1'
9
+ ): OpenApiSchema {
6
10
  const schema: OpenApiSchema = {
7
11
  type: columnTypeToOpenApiType(col),
8
12
  format: columnTypeToOpenApiFormat(col),
9
13
  };
10
14
 
11
- if (!col.notNull) {
12
- schema.nullable = true;
13
- }
15
+ const nullable = isNullableColumn(col);
16
+ const result = applyNullability(schema, nullable, dialect);
14
17
 
15
18
  if (col.comment) {
16
- schema.description = col.comment;
19
+ result.description = col.comment;
17
20
  }
18
21
 
19
22
  if (col.type.toUpperCase() === 'ENUM' && col.args && Array.isArray(col.args) && col.args.every(v => typeof v === 'string')) {
20
- schema.enum = col.args as string[];
23
+ result.enum = col.args as string[];
21
24
  }
22
25
 
23
26
  const args = col.args;
@@ -25,10 +28,10 @@ export function columnToOpenApiSchema(col: ColumnDef): OpenApiSchema {
25
28
  if (col.type.toUpperCase() === 'VARCHAR' || col.type.toUpperCase() === 'CHAR') {
26
29
  const length = args[0] as number;
27
30
  if (length) {
28
- schema.example = 'a'.repeat(length);
31
+ result.example = 'a'.repeat(length);
29
32
  }
30
33
  }
31
34
  }
32
35
 
33
- return schema;
36
+ return result;
34
37
  }
@@ -1,13 +1,14 @@
1
1
  import type { TableDef } from '../../../schema/table.js';
2
2
  import type { EntityConstructor } from '../../../orm/entity-metadata.js';
3
- import type { OpenApiSchema } from '../types.js';
3
+ import type { OpenApiSchema, OpenApiDialect } from '../types.js';
4
4
  import { columnToOpenApiSchema } from './column.js';
5
5
  import { getColumnMap } from './base.js';
6
6
 
7
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
8
  export function dtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExclude extends keyof any>(
9
9
  target: T,
10
- exclude?: TExclude[]
10
+ exclude?: TExclude[],
11
+ dialect: OpenApiDialect = 'openapi-3.1'
11
12
  ): OpenApiSchema {
12
13
  const columns = getColumnMap(target);
13
14
  const properties: Record<string, OpenApiSchema> = {};
@@ -18,7 +19,7 @@ export function dtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExcl
18
19
  continue;
19
20
  }
20
21
 
21
- properties[key] = columnToOpenApiSchema(col);
22
+ properties[key] = columnToOpenApiSchema(col, dialect);
22
23
 
23
24
  if (col.notNull || col.primary) {
24
25
  required.push(key);
@@ -35,7 +36,8 @@ export function dtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExcl
35
36
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
37
  export function createDtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExclude extends keyof any>(
37
38
  target: T,
38
- exclude?: TExclude[]
39
+ exclude?: TExclude[],
40
+ dialect: OpenApiDialect = 'openapi-3.1'
39
41
  ): OpenApiSchema {
40
42
  const columns = getColumnMap(target);
41
43
  const properties: Record<string, OpenApiSchema> = {};
@@ -50,7 +52,7 @@ export function createDtoToOpenApiSchema<T extends TableDef | EntityConstructor,
50
52
  continue;
51
53
  }
52
54
 
53
- properties[key] = columnToOpenApiSchema(col);
55
+ properties[key] = columnToOpenApiSchema(col, dialect);
54
56
 
55
57
  if (col.notNull && !col.default) {
56
58
  required.push(key);
@@ -67,7 +69,8 @@ export function createDtoToOpenApiSchema<T extends TableDef | EntityConstructor,
67
69
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
70
  export function updateDtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExclude extends keyof any>(
69
71
  target: T,
70
- exclude?: TExclude[]
72
+ exclude?: TExclude[],
73
+ dialect: OpenApiDialect = 'openapi-3.1'
71
74
  ): OpenApiSchema {
72
75
  const columns = getColumnMap(target);
73
76
  const properties: Record<string, OpenApiSchema> = {};
@@ -81,10 +84,7 @@ export function updateDtoToOpenApiSchema<T extends TableDef | EntityConstructor,
81
84
  continue;
82
85
  }
83
86
 
84
- properties[key] = {
85
- ...columnToOpenApiSchema(col),
86
- ...(!col.notNull ? { nullable: true } : {}),
87
- };
87
+ properties[key] = columnToOpenApiSchema(col, dialect);
88
88
  }
89
89
 
90
90
  return {
@@ -1,54 +1,45 @@
1
1
  import type { TableDef } from '../../../schema/table.js';
2
2
  import type { ColumnDef } from '../../../schema/column-types.js';
3
3
  import type { EntityConstructor } from '../../../orm/entity-metadata.js';
4
- import type { OpenApiSchema } from '../types.js';
5
- import { columnTypeToOpenApiType } from '../type-mappings.js';
4
+ import type { OpenApiSchema, OpenApiDialect } from '../types.js';
5
+ import { columnTypeToOpenApiType, columnTypeToOpenApiFormat } from '../type-mappings.js';
6
6
  import { getColumnMap } from './base.js';
7
+ import { applyNullability, isNullableColumn } from '../utilities.js';
7
8
 
8
9
  function filterFieldToOpenApiSchema(col: ColumnDef): OpenApiSchema {
9
- const normalizedType = col.type.toUpperCase();
10
- columnTypeToOpenApiType(col);
11
-
12
- let filterProperties: Record<string, OpenApiSchema> = {};
10
+ const openApiType = columnTypeToOpenApiType(col);
11
+ const openApiFormat = columnTypeToOpenApiFormat(col);
13
12
 
14
- if (['INT', 'INTEGER', 'BIGINT', 'DECIMAL', 'FLOAT', 'DOUBLE'].includes(normalizedType)) {
15
- filterProperties = {
16
- equals: { type: 'number' },
17
- not: { type: 'number' },
18
- in: { type: 'array', items: { type: 'number' } },
19
- notIn: { type: 'array', items: { type: 'number' } },
20
- lt: { type: 'number' },
21
- lte: { type: 'number' },
22
- gt: { type: 'number' },
23
- gte: { type: 'number' },
24
- };
25
- } else if (['BOOLEAN'].includes(normalizedType)) {
26
- filterProperties = {
27
- equals: { type: 'boolean' },
28
- not: { type: 'boolean' },
29
- };
30
- } else if (['DATE', 'DATETIME', 'TIMESTAMP', 'TIMESTAMPTZ'].includes(normalizedType)) {
31
- filterProperties = {
32
- equals: { type: 'string', format: 'date-time' },
33
- not: { type: 'string', format: 'date-time' },
34
- in: { type: 'array', items: { type: 'string', format: 'date-time' } },
35
- notIn: { type: 'array', items: { type: 'string', format: 'date-time' } },
36
- lt: { type: 'string', format: 'date-time' },
37
- lte: { type: 'string', format: 'date-time' },
38
- gt: { type: 'string', format: 'date-time' },
39
- gte: { type: 'string', format: 'date-time' },
40
- };
41
- } else {
42
- filterProperties = {
43
- equals: { type: 'string' },
44
- not: { type: 'string' },
45
- in: { type: 'array', items: { type: 'string' } },
46
- notIn: { type: 'array', items: { type: 'string' } },
47
- contains: { type: 'string' },
48
- startsWith: { type: 'string' },
49
- endsWith: { type: 'string' },
50
- mode: { type: 'string', enum: ['default', 'insensitive'] },
51
- };
13
+ const filterProperties: Record<string, OpenApiSchema> = {};
14
+
15
+ filterProperties.equals = { type: openApiType, format: openApiFormat };
16
+ filterProperties.not = { type: openApiType, format: openApiFormat };
17
+ filterProperties.in = { type: 'array', items: { type: openApiType, format: openApiFormat } };
18
+ filterProperties.notIn = { type: 'array', items: { type: openApiType, format: openApiFormat } };
19
+
20
+ const isNumeric = openApiType === 'integer' || openApiType === 'number';
21
+ const isDateOrTime = openApiType === 'string' && (openApiFormat === 'date' || openApiFormat === 'date-time');
22
+ const isString = openApiType === 'string' && !isDateOrTime;
23
+
24
+ if (isNumeric) {
25
+ filterProperties.lt = { type: openApiType, format: openApiFormat };
26
+ filterProperties.lte = { type: openApiType, format: openApiFormat };
27
+ filterProperties.gt = { type: openApiType, format: openApiFormat };
28
+ filterProperties.gte = { type: openApiType, format: openApiFormat };
29
+ }
30
+
31
+ if (isDateOrTime) {
32
+ filterProperties.lt = { type: openApiType, format: openApiFormat };
33
+ filterProperties.lte = { type: openApiType, format: openApiFormat };
34
+ filterProperties.gt = { type: openApiType, format: openApiFormat };
35
+ filterProperties.gte = { type: openApiType, format: openApiFormat };
36
+ }
37
+
38
+ if (isString) {
39
+ filterProperties.contains = { type: 'string' };
40
+ filterProperties.startsWith = { type: 'string' };
41
+ filterProperties.endsWith = { type: 'string' };
42
+ filterProperties.mode = { type: 'string', enum: ['default', 'insensitive'] };
52
43
  }
53
44
 
54
45
  return {
@@ -57,14 +48,24 @@ function filterFieldToOpenApiSchema(col: ColumnDef): OpenApiSchema {
57
48
  };
58
49
  }
59
50
 
51
+ export function columnToFilterSchema(
52
+ col: ColumnDef,
53
+ dialect: OpenApiDialect = 'openapi-3.1'
54
+ ): OpenApiSchema {
55
+ const filterSchema = filterFieldToOpenApiSchema(col);
56
+ const nullable = isNullableColumn(col);
57
+ return applyNullability(filterSchema, nullable, dialect);
58
+ }
59
+
60
60
  export function whereInputToOpenApiSchema<T extends TableDef | EntityConstructor>(
61
- target: T
61
+ target: T,
62
+ dialect: OpenApiDialect = 'openapi-3.1'
62
63
  ): OpenApiSchema {
63
64
  const columns = getColumnMap(target);
64
65
  const properties: Record<string, OpenApiSchema> = {};
65
66
 
66
67
  for (const [key, col] of Object.entries(columns)) {
67
- properties[key] = filterFieldToOpenApiSchema(col);
68
+ properties[key] = columnToFilterSchema(col, dialect);
68
69
  }
69
70
 
70
71
  return {
@@ -7,8 +7,9 @@ import type {
7
7
  HasOneRelation,
8
8
  BelongsToManyRelation
9
9
  } from '../../../schema/relation.js';
10
- import type { OpenApiSchema, OpenApiComponent } from '../types.js';
10
+ import type { OpenApiSchema, OpenApiComponent, OpenApiDialect, OpenApiParameterObject, OpenApiResponseObject } from '../types.js';
11
11
  import { columnToOpenApiSchema } from './column.js';
12
+ import { columnToFilterSchema } from './filter.js';
12
13
  import { getColumnMap } from './base.js';
13
14
  import { RelationKinds } from '../../../schema/relation.js';
14
15
 
@@ -125,10 +126,7 @@ export function updateDtoWithRelationsToOpenApiSchema<T extends TableDef | Entit
125
126
  continue;
126
127
  }
127
128
 
128
- properties[key] = {
129
- ...columnToOpenApiSchema(col),
130
- nullable: true,
131
- };
129
+ properties[key] = columnToOpenApiSchema(col);
132
130
  }
133
131
 
134
132
  const tableDef = target as TableDef;
@@ -159,10 +157,7 @@ function updateDtoToOpenApiSchemaForComponent(
159
157
  continue;
160
158
  }
161
159
 
162
- properties[key] = {
163
- ...columnToOpenApiSchema(col),
164
- nullable: true,
165
- };
160
+ properties[key] = columnToOpenApiSchema(col);
166
161
  }
167
162
 
168
163
  return {
@@ -270,7 +265,8 @@ function whereInputWithRelationsToOpenApiSchema(
270
265
  relationInclude?: string[];
271
266
  maxDepth?: number;
272
267
  prefix?: string;
273
- }
268
+ },
269
+ dialect: OpenApiDialect = 'openapi-3.1'
274
270
  ): OpenApiSchema {
275
271
  const columns = getColumnMap(target);
276
272
  const properties: Record<string, OpenApiSchema> = {};
@@ -285,7 +281,7 @@ function whereInputWithRelationsToOpenApiSchema(
285
281
  continue;
286
282
  }
287
283
 
288
- properties[key] = columnToOpenApiSchema(col);
284
+ properties[key] = columnToFilterSchema(col, dialect);
289
285
  }
290
286
 
291
287
  const tableDef = target as TableDef;
@@ -300,9 +296,9 @@ function whereInputWithRelationsToOpenApiSchema(
300
296
  }
301
297
 
302
298
  properties[relationName] = relationFilterToOpenApiSchema(relation, {
303
- exclude: options?.columnExclude,
304
- include: options?.columnInclude,
305
- });
299
+ exclude: options.columnExclude,
300
+ include: options.columnInclude,
301
+ }, dialect);
306
302
  }
307
303
  }
308
304
 
@@ -317,10 +313,11 @@ function relationFilterToOpenApiSchema(
317
313
  options?: {
318
314
  exclude?: string[];
319
315
  include?: string[];
320
- }
316
+ },
317
+ dialect: OpenApiDialect = 'openapi-3.1'
321
318
  ): OpenApiSchema {
322
319
  if (relation.type === RelationKinds.BelongsTo || relation.type === RelationKinds.HasOne) {
323
- return singleRelationFilterToOpenApiSchema((relation as BelongsToRelation | HasOneRelation).target, options);
320
+ return singleRelationFilterToOpenApiSchema((relation as BelongsToRelation | HasOneRelation).target, options, dialect);
324
321
  }
325
322
 
326
323
  if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
@@ -332,7 +329,8 @@ function relationFilterToOpenApiSchema(
332
329
 
333
330
  function singleRelationFilterToOpenApiSchema(
334
331
  target: TableDef | EntityConstructor,
335
- options?: { exclude?: string[]; include?: string[] }
332
+ options?: { exclude?: string[]; include?: string[] },
333
+ dialect: OpenApiDialect = 'openapi-3.1'
336
334
  ): OpenApiSchema {
337
335
  const columns = getColumnMap(target);
338
336
  const properties: Record<string, OpenApiSchema> = {};
@@ -346,7 +344,7 @@ function singleRelationFilterToOpenApiSchema(
346
344
  continue;
347
345
  }
348
346
 
349
- properties[key] = columnToOpenApiSchema(col);
347
+ properties[key] = columnToFilterSchema(col, dialect);
350
348
  }
351
349
 
352
350
  return {
@@ -395,7 +393,7 @@ function generateNestedProperties(
395
393
  const properties: Record<string, OpenApiSchema> = {};
396
394
 
397
395
  for (const [key, col] of Object.entries(columns)) {
398
- properties[key] = columnToOpenApiSchema(col);
396
+ properties[key] = columnToFilterSchema(col);
399
397
  }
400
398
 
401
399
  return properties;
@@ -403,8 +401,8 @@ function generateNestedProperties(
403
401
 
404
402
  export function createApiComponentsSection(
405
403
  schemas: Record<string, OpenApiSchema>,
406
- parameters?: Record<string, OpenApiSchema>,
407
- responses?: Record<string, OpenApiSchema>
404
+ parameters?: Record<string, OpenApiParameterObject>,
405
+ responses?: Record<string, OpenApiResponseObject>
408
406
  ): OpenApiComponent {
409
407
  const component: OpenApiComponent = {};
410
408
 
@@ -439,6 +437,94 @@ export function responseToRef(responseName: string): ComponentReference {
439
437
  return createRef(`responses/${responseName}`);
440
438
  }
441
439
 
440
+ export function canonicalizeSchema(schema: OpenApiSchema): OpenApiSchema {
441
+ if (typeof schema !== 'object' || schema === null) {
442
+ return schema;
443
+ }
444
+
445
+ if (Array.isArray(schema)) {
446
+ return schema.map(canonicalizeSchema) as unknown as OpenApiSchema;
447
+ }
448
+
449
+ const canonical: OpenApiSchema = {};
450
+
451
+ const keys = Object.keys(schema).sort();
452
+
453
+ for (const key of keys) {
454
+ if (key === 'description' || key === 'example') {
455
+ continue;
456
+ }
457
+
458
+ const value = schema[key as keyof OpenApiSchema];
459
+
460
+ if (typeof value === 'object' && value !== null) {
461
+ (canonical as Record<string, unknown>)[key] = canonicalizeSchema(value as OpenApiSchema);
462
+ } else {
463
+ (canonical as Record<string, unknown>)[key] = value;
464
+ }
465
+ }
466
+
467
+ return canonical;
468
+ }
469
+
470
+ export function computeSchemaHash(schema: OpenApiSchema): string {
471
+ const canonical = canonicalizeSchema(schema);
472
+ const json = JSON.stringify(canonical);
473
+ let hash = 0;
474
+
475
+ for (let i = 0; i < json.length; i++) {
476
+ const char = json.charCodeAt(i);
477
+ hash = ((hash << 5) - hash) + char;
478
+ hash = hash & hash;
479
+ }
480
+
481
+ const hex = Math.abs(hash).toString(16);
482
+ return hex.padStart(8, '0').slice(0, 6);
483
+ }
484
+
485
+ interface DeterministicNamingState {
486
+ contentHashToName: Map<string, string>;
487
+ nameToContentHash: Map<string, string>;
488
+ }
489
+
490
+ export function createDeterministicNamingState(): DeterministicNamingState {
491
+ return {
492
+ contentHashToName: new Map(),
493
+ nameToContentHash: new Map(),
494
+ };
495
+ }
496
+
497
+ export function getDeterministicComponentName(
498
+ baseName: string,
499
+ schema: OpenApiSchema,
500
+ state: DeterministicNamingState
501
+ ): string {
502
+ const hash = computeSchemaHash(schema);
503
+ const normalizedBase = baseName.replace(/[^A-Za-z0-9_]/g, '');
504
+
505
+ const existingName = state.contentHashToName.get(hash);
506
+ if (existingName) {
507
+ return existingName;
508
+ }
509
+
510
+ if (!state.nameToContentHash.has(normalizedBase)) {
511
+ state.contentHashToName.set(hash, normalizedBase);
512
+ state.nameToContentHash.set(normalizedBase, hash);
513
+ return normalizedBase;
514
+ }
515
+
516
+ const existingHash = state.nameToContentHash.get(normalizedBase)!;
517
+ if (existingHash === hash) {
518
+ return normalizedBase;
519
+ }
520
+
521
+ const uniqueName = `${normalizedBase}_${hash}`;
522
+ state.contentHashToName.set(hash, uniqueName);
523
+ state.nameToContentHash.set(uniqueName, hash);
524
+
525
+ return uniqueName;
526
+ }
527
+
442
528
  export function replaceWithRefs(
443
529
  schema: OpenApiSchema,
444
530
  schemaMap: Record<string, OpenApiSchema>,