metal-orm 1.0.89 → 1.0.90

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.
Files changed (49) hide show
  1. package/dist/index.cjs +2968 -2983
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +765 -246
  4. package/dist/index.d.ts +765 -246
  5. package/dist/index.js +2913 -2975
  6. package/dist/index.js.map +1 -1
  7. package/package.json +3 -2
  8. package/src/codegen/typescript.ts +29 -40
  9. package/src/core/ast/expression-builders.ts +34 -53
  10. package/src/core/ast/expression-nodes.ts +51 -72
  11. package/src/core/ast/expression-visitor.ts +219 -252
  12. package/src/core/ast/expression.ts +20 -21
  13. package/src/core/dialect/abstract.ts +55 -81
  14. package/src/core/execution/db-executor.ts +4 -5
  15. package/src/core/execution/executors/mysql-executor.ts +7 -9
  16. package/src/decorators/bootstrap.ts +11 -8
  17. package/src/dto/apply-filter.ts +281 -0
  18. package/src/dto/dto-types.ts +229 -0
  19. package/src/dto/filter-types.ts +193 -0
  20. package/src/dto/index.ts +97 -0
  21. package/src/dto/openapi/generators/base.ts +29 -0
  22. package/src/dto/openapi/generators/column.ts +34 -0
  23. package/src/dto/openapi/generators/dto.ts +94 -0
  24. package/src/dto/openapi/generators/filter.ts +74 -0
  25. package/src/dto/openapi/generators/nested-dto.ts +532 -0
  26. package/src/dto/openapi/generators/pagination.ts +111 -0
  27. package/src/dto/openapi/generators/relation-filter.ts +210 -0
  28. package/src/dto/openapi/index.ts +17 -0
  29. package/src/dto/openapi/type-mappings.ts +191 -0
  30. package/src/dto/openapi/types.ts +83 -0
  31. package/src/dto/openapi/utilities.ts +45 -0
  32. package/src/dto/pagination-utils.ts +150 -0
  33. package/src/dto/transform.ts +193 -0
  34. package/src/index.ts +67 -65
  35. package/src/orm/unit-of-work.ts +13 -25
  36. package/src/query-builder/query-ast-service.ts +287 -300
  37. package/src/query-builder/relation-filter-utils.ts +159 -160
  38. package/src/query-builder/select.ts +137 -192
  39. package/src/core/ast/ast-validation.ts +0 -19
  40. package/src/core/ast/param-proxy.ts +0 -47
  41. package/src/core/ast/query-visitor.ts +0 -273
  42. package/src/openapi/index.ts +0 -4
  43. package/src/openapi/query-parameters.ts +0 -207
  44. package/src/openapi/schema-extractor-input.ts +0 -193
  45. package/src/openapi/schema-extractor-output.ts +0 -427
  46. package/src/openapi/schema-extractor-utils.ts +0 -110
  47. package/src/openapi/schema-extractor.ts +0 -120
  48. package/src/openapi/schema-types.ts +0 -187
  49. package/src/openapi/type-mappers.ts +0 -227
@@ -66,9 +66,7 @@ import { SelectProjectionFacet } from './select/projection-facet.js';
66
66
  import { SelectPredicateFacet } from './select/predicate-facet.js';
67
67
  import { SelectCTEFacet } from './select/cte-facet.js';
68
68
  import { SelectSetOpFacet } from './select/setop-facet.js';
69
- import { SelectRelationFacet } from './select/relation-facet.js';
70
- import { buildFilterParameters, extractSchema, SchemaOptions, OpenApiSchemaBundle } from '../openapi/index.js';
71
- import { findFirstParamOperandName } from '../core/ast/ast-validation.js';
69
+ import { SelectRelationFacet } from './select/relation-facet.js';
72
70
 
73
71
  type ColumnSelectionValue =
74
72
  | ColumnDef
@@ -82,14 +80,10 @@ type SelectionValueType<TValue> =
82
80
  TValue extends ColumnDef ? ColumnToTs<TValue> :
83
81
  unknown;
84
82
 
85
- type SelectionResult<TSelection extends Record<string, ColumnSelectionValue>> = {
86
- [K in keyof TSelection]: SelectionValueType<TSelection[K]>;
87
- };
88
-
89
- type ParamOperandCompileOptions = {
90
- allowParamOperands?: boolean;
91
- };
92
-
83
+ type SelectionResult<TSelection extends Record<string, ColumnSelectionValue>> = {
84
+ [K in keyof TSelection]: SelectionValueType<TSelection[K]>;
85
+ };
86
+
93
87
  type SelectionFromKeys<
94
88
  TTable extends TableDef,
95
89
  K extends keyof TTable['columns'] & string
@@ -718,138 +712,120 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
718
712
  return this.env.table as TTable;
719
713
  }
720
714
 
721
- /**
722
- * Ensures that if no columns are selected, all columns from the table are selected by default.
723
- */
724
- private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
725
- const columns = this.context.state.ast.columns;
726
- if (!columns || columns.length === 0) {
727
- const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
728
- return this.select(...columnKeys);
729
- }
730
- return this;
731
- }
732
-
733
- /**
734
- * Validates that the query does not contain Param operands.
735
- * Param proxies are only for schema generation, not execution.
736
- */
737
- private validateNoParamOperands(): void {
738
- const ast = this.context.hydration.applyToAst(this.context.state.ast);
739
- const paramName = findFirstParamOperandName(ast);
740
- if (paramName) {
741
- throw new Error(`Cannot execute query containing Param operand "${paramName}". Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.`);
742
- }
743
- }
744
-
745
- /**
746
- * Executes the query and returns hydrated results.
747
- * If the builder was created with an entity constructor (e.g. via selectFromEntity),
748
- * this will automatically return fully materialized entity instances.
749
- *
750
- * @param ctx - ORM session context
751
- * @returns Promise of entity instances (or objects if generic T is not an entity)
752
- * @example
753
- * const users = await selectFromEntity(User).execute(session);
754
- * // users is User[]
755
- * users[0] instanceof User; // true
756
- */
757
- async execute(ctx: OrmSession): Promise<T[]> {
758
- this.validateNoParamOperands();
759
- if (this.entityConstructor) {
760
- return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
761
- }
762
- const builder = this.ensureDefaultSelection();
763
- return executeHydrated(ctx, builder) as unknown as T[];
764
- }
765
-
766
- /**
767
- * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
768
- * Use this if you want raw data even when using selectFromEntity.
769
- *
770
- * @param ctx - ORM session context
771
- * @returns Promise of plain entity instances
772
- * @example
773
- * const rows = await selectFromEntity(User).executePlain(session);
774
- * // rows is EntityInstance<UserTable>[] (plain objects)
775
- * rows[0] instanceof User; // false
776
- */
777
- async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
778
- this.validateNoParamOperands();
779
- const builder = this.ensureDefaultSelection();
780
- const rows = await executeHydratedPlain(ctx, builder);
781
- return rows as EntityInstance<TTable>[];
782
- }
783
-
784
- /**
785
- * Executes the query and returns results as real class instances.
786
- * Unlike execute(), this returns actual instances of the decorated entity class
787
- * with working methods and proper instanceof checks.
788
- * @param entityClass - The entity class constructor
789
- * @param ctx - ORM session context
790
- * @returns Promise of entity class instances
791
- * @example
792
- * const users = await selectFromEntity(User)
793
- * .include('posts')
794
- * .executeAs(User, session);
795
- * users[0] instanceof User; // true!
796
- * users[0].getFullName(); // works!
797
- */
798
- async executeAs<TEntity extends object>(
799
- entityClass: EntityConstructor<TEntity>,
800
- ctx: OrmSession
801
- ): Promise<TEntity[]> {
802
- this.validateNoParamOperands();
803
- const builder = this.ensureDefaultSelection();
804
- const results = await executeHydrated(ctx, builder);
805
- return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
806
- }
807
-
808
- /**
809
- * Executes a count query for the current builder without LIMIT/OFFSET clauses.
810
- *
811
- * @example
812
- * const total = await qb.count(session);
813
- */
814
- async count(session: OrmSession): Promise<number> {
815
- this.validateNoParamOperands();
816
- return executeCount(this.context, this.env, session);
817
- }
818
-
819
- /**
820
- * Executes the query and returns both the paged items and the total.
821
- *
822
- * @example
823
- * const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
824
- */
825
- async executePaged(
826
- session: OrmSession,
827
- options: { page: number; pageSize: number }
828
- ): Promise<PaginatedResult<T>> {
829
- this.validateNoParamOperands();
830
- const builder = this.ensureDefaultSelection();
831
- return executePagedQuery(builder, session, options, sess => builder.count(sess));
832
- }
833
-
834
- /**
835
- * Executes the query with provided execution and hydration contexts
836
- * @param execCtx - Execution context
837
- * @param hydCtx - Hydration context
838
- * @returns Promise of entity instances
839
- * @example
840
- * const execCtx = new ExecutionContext(session);
841
- * const hydCtx = new HydrationContext();
842
- * const users = await qb.executeWithContexts(execCtx, hydCtx);
843
- */
844
- async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
845
- this.validateNoParamOperands();
846
- const builder = this.ensureDefaultSelection();
847
- const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
848
- if (this.entityConstructor) {
849
- return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
850
- }
851
- return results as unknown as T[];
852
- }
715
+ /**
716
+ * Ensures that if no columns are selected, all columns from the table are selected by default.
717
+ */
718
+ private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
719
+ const columns = this.context.state.ast.columns;
720
+ if (!columns || columns.length === 0) {
721
+ const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
722
+ return this.select(...columnKeys);
723
+ }
724
+ return this;
725
+ }
726
+
727
+ /**
728
+ * Executes the query and returns hydrated results.
729
+ * If the builder was created with an entity constructor (e.g. via selectFromEntity),
730
+ * this will automatically return fully materialized entity instances.
731
+ *
732
+ * @param ctx - ORM session context
733
+ * @returns Promise of entity instances (or objects if generic T is not an entity)
734
+ * @example
735
+ * const users = await selectFromEntity(User).execute(session);
736
+ * // users is User[]
737
+ * users[0] instanceof User; // true
738
+ */
739
+ async execute(ctx: OrmSession): Promise<T[]> {
740
+ if (this.entityConstructor) {
741
+ return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
742
+ }
743
+ const builder = this.ensureDefaultSelection();
744
+ return executeHydrated(ctx, builder) as unknown as T[];
745
+ }
746
+
747
+ /**
748
+ * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
749
+ * Use this if you want raw data even when using selectFromEntity.
750
+ *
751
+ * @param ctx - ORM session context
752
+ * @returns Promise of plain entity instances
753
+ * @example
754
+ * const rows = await selectFromEntity(User).executePlain(session);
755
+ * // rows is EntityInstance<UserTable>[] (plain objects)
756
+ * rows[0] instanceof User; // false
757
+ */
758
+ async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
759
+ const builder = this.ensureDefaultSelection();
760
+ const rows = await executeHydratedPlain(ctx, builder);
761
+ return rows as EntityInstance<TTable>[];
762
+ }
763
+
764
+ /**
765
+ * Executes the query and returns results as real class instances.
766
+ * Unlike execute(), this returns actual instances of the decorated entity class
767
+ * with working methods and proper instanceof checks.
768
+ * @param entityClass - The entity class constructor
769
+ * @param ctx - ORM session context
770
+ * @returns Promise of entity class instances
771
+ * @example
772
+ * const users = await selectFromEntity(User)
773
+ * .include('posts')
774
+ * .executeAs(User, session);
775
+ * users[0] instanceof User; // true!
776
+ * users[0].getFullName(); // works!
777
+ */
778
+ async executeAs<TEntity extends object>(
779
+ entityClass: EntityConstructor<TEntity>,
780
+ ctx: OrmSession
781
+ ): Promise<TEntity[]> {
782
+ const builder = this.ensureDefaultSelection();
783
+ const results = await executeHydrated(ctx, builder);
784
+ return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
785
+ }
786
+
787
+ /**
788
+ * Executes a count query for the current builder without LIMIT/OFFSET clauses.
789
+ *
790
+ * @example
791
+ * const total = await qb.count(session);
792
+ */
793
+ async count(session: OrmSession): Promise<number> {
794
+ return executeCount(this.context, this.env, session);
795
+ }
796
+
797
+ /**
798
+ * Executes the query and returns both the paged items and the total.
799
+ *
800
+ * @example
801
+ * const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
802
+ */
803
+ async executePaged(
804
+ session: OrmSession,
805
+ options: { page: number; pageSize: number }
806
+ ): Promise<PaginatedResult<T>> {
807
+ const builder = this.ensureDefaultSelection();
808
+ return executePagedQuery(builder, session, options, sess => builder.count(sess));
809
+ }
810
+
811
+ /**
812
+ * Executes the query with provided execution and hydration contexts
813
+ * @param execCtx - Execution context
814
+ * @param hydCtx - Hydration context
815
+ * @returns Promise of entity instances
816
+ * @example
817
+ * const execCtx = new ExecutionContext(session);
818
+ * const hydCtx = new HydrationContext();
819
+ * const users = await qb.executeWithContexts(execCtx, hydCtx);
820
+ */
821
+ async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
822
+ const builder = this.ensureDefaultSelection();
823
+ const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
824
+ if (this.entityConstructor) {
825
+ return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
826
+ }
827
+ return results as unknown as T[];
828
+ }
853
829
 
854
830
  /**
855
831
  * Adds a WHERE condition to the query
@@ -1113,18 +1089,10 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
1113
1089
  * .compile('postgres');
1114
1090
  * console.log(compiled.sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
1115
1091
  */
1116
- compile(dialect: SelectDialectInput, options?: ParamOperandCompileOptions): CompiledQuery {
1117
- const resolved = resolveDialectInput(dialect);
1118
- const ast = this.getAST();
1119
- if (!options?.allowParamOperands) {
1120
- const paramName = findFirstParamOperandName(ast);
1121
- if (paramName) {
1122
- throw new Error(`Cannot compile query containing Param operand "${paramName}". Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.`);
1123
- }
1124
- return resolved.compileSelect(ast);
1125
- }
1126
- return resolved.compileSelectWithOptions(ast, { allowParams: true });
1127
- }
1092
+ compile(dialect: SelectDialectInput): CompiledQuery {
1093
+ const resolved = resolveDialectInput(dialect);
1094
+ return resolved.compileSelect(this.getAST());
1095
+ }
1128
1096
 
1129
1097
  /**
1130
1098
  * Converts the query to SQL string for a specific dialect
@@ -1136,43 +1104,20 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
1136
1104
  * .toSql('postgres');
1137
1105
  * console.log(sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
1138
1106
  */
1139
- toSql(dialect: SelectDialectInput, options?: ParamOperandCompileOptions): string {
1140
- return this.compile(dialect, options).sql;
1141
- }
1142
-
1143
- /**
1144
- * Gets hydration plan for query
1145
- * @returns Hydration plan or undefined if none exists
1146
- * @example
1147
- * const plan = qb.include('posts').getHydrationPlan();
1148
- * console.log(plan?.relations); // Information about included relations
1149
- */
1150
- getHydrationPlan(): HydrationPlan | undefined {
1151
- return this.context.hydration.getPlan();
1152
- }
1153
-
1154
- /**
1155
- * Gets OpenAPI 3.1 JSON Schemas for query output and optional input payloads
1156
- * @param options - Schema generation options
1157
- * @returns OpenAPI 3.1 JSON Schemas for query output and input payloads
1158
- * @example
1159
- * const { output } = qb.select('id', 'title', 'author').getSchema();
1160
- * console.log(JSON.stringify(output, null, 2));
1161
- */
1162
- getSchema(options?: SchemaOptions): OpenApiSchemaBundle {
1163
- const plan = this.context.hydration.getPlan();
1164
- const bundle = extractSchema(this.env.table, plan, this.context.state.ast.columns, options);
1165
- const parameters = buildFilterParameters(
1166
- this.env.table,
1167
- this.context.state.ast.where,
1168
- this.context.state.ast.from,
1169
- options ?? {}
1170
- );
1171
- if (parameters.length) {
1172
- return { ...bundle, parameters };
1173
- }
1174
- return bundle;
1175
- }
1107
+ toSql(dialect: SelectDialectInput): string {
1108
+ return this.compile(dialect).sql;
1109
+ }
1110
+
1111
+ /**
1112
+ * Gets the hydration plan for the query
1113
+ * @returns Hydration plan or undefined if none exists
1114
+ * @example
1115
+ * const plan = qb.include('posts').getHydrationPlan();
1116
+ * console.log(plan?.relations); // Information about included relations
1117
+ */
1118
+ getHydrationPlan(): HydrationPlan | undefined {
1119
+ return this.context.hydration.getPlan();
1120
+ }
1176
1121
 
1177
1122
  /**
1178
1123
  * Gets the Abstract Syntax Tree (AST) representation of the query
@@ -1,19 +0,0 @@
1
- import type { SelectQueryNode } from './query.js';
2
- import { visitSelectQuery } from './query-visitor.js';
3
-
4
- export const findFirstParamOperandName = (ast: SelectQueryNode): string | undefined => {
5
- let name: string | undefined;
6
-
7
- visitSelectQuery(ast, {
8
- visitParam: (node) => {
9
- if (!name) {
10
- name = node.name;
11
- }
12
- }
13
- });
14
-
15
- return name;
16
- };
17
-
18
- export const hasParamOperandsInQuery = (ast: SelectQueryNode): boolean =>
19
- !!findFirstParamOperandName(ast);
@@ -1,47 +0,0 @@
1
- import type { ParamNode } from './expression-nodes.js';
2
-
3
- export type ParamProxy = ParamNode & {
4
- [key: string]: ParamProxy;
5
- };
6
-
7
- export type ParamProxyRoot = {
8
- [key: string]: ParamProxy;
9
- };
10
-
11
- const buildParamProxy = (name: string): ParamProxy => {
12
- const target: ParamNode = { type: 'Param', name };
13
- return new Proxy(target, {
14
- get(t, prop, receiver) {
15
- if (prop === 'then') return undefined;
16
- if (typeof prop === 'symbol') {
17
- return Reflect.get(t, prop, receiver);
18
- }
19
- if (typeof prop === 'string' && prop.startsWith('$')) {
20
- const trimmed = prop.slice(1);
21
- const nextName = name ? `${name}.${trimmed}` : trimmed;
22
- return buildParamProxy(nextName);
23
- }
24
- if (prop in t && name === '') {
25
- return (t as unknown as Record<string, unknown>)[prop];
26
- }
27
- const nextName = name ? `${name}.${prop}` : prop;
28
- return buildParamProxy(nextName);
29
- }
30
- }) as ParamProxy;
31
- };
32
-
33
- export const createParamProxy = (): ParamProxyRoot => {
34
- const target: Record<string, unknown> = {};
35
- return new Proxy(target, {
36
- get(t, prop, receiver) {
37
- if (prop === 'then') return undefined;
38
- if (typeof prop === 'symbol') {
39
- return Reflect.get(t, prop, receiver);
40
- }
41
- if (typeof prop === 'string' && prop.startsWith('$')) {
42
- return buildParamProxy(prop.slice(1));
43
- }
44
- return buildParamProxy(String(prop));
45
- }
46
- }) as ParamProxyRoot;
47
- };