metal-orm 1.0.89 → 1.0.91

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 (57) hide show
  1. package/dist/index.cjs +2968 -2983
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +784 -251
  4. package/dist/index.d.ts +784 -251
  5. package/dist/index.js +2913 -2975
  6. package/dist/index.js.map +1 -1
  7. package/package.json +6 -3
  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/ddl/introspect/utils.ts +45 -45
  14. package/src/core/dialect/abstract.ts +55 -81
  15. package/src/core/execution/db-executor.ts +4 -5
  16. package/src/core/execution/executors/mysql-executor.ts +7 -9
  17. package/src/decorators/bootstrap.ts +29 -26
  18. package/src/dto/apply-filter.ts +279 -0
  19. package/src/dto/dto-types.ts +229 -0
  20. package/src/dto/filter-types.ts +193 -0
  21. package/src/dto/index.ts +97 -0
  22. package/src/dto/openapi/generators/base.ts +29 -0
  23. package/src/dto/openapi/generators/column.ts +34 -0
  24. package/src/dto/openapi/generators/dto.ts +94 -0
  25. package/src/dto/openapi/generators/filter.ts +74 -0
  26. package/src/dto/openapi/generators/nested-dto.ts +532 -0
  27. package/src/dto/openapi/generators/pagination.ts +111 -0
  28. package/src/dto/openapi/generators/relation-filter.ts +210 -0
  29. package/src/dto/openapi/index.ts +17 -0
  30. package/src/dto/openapi/type-mappings.ts +191 -0
  31. package/src/dto/openapi/types.ts +90 -0
  32. package/src/dto/openapi/utilities.ts +45 -0
  33. package/src/dto/pagination-utils.ts +150 -0
  34. package/src/dto/transform.ts +197 -0
  35. package/src/index.ts +5 -3
  36. package/src/orm/entity-context.ts +9 -9
  37. package/src/orm/entity.ts +74 -74
  38. package/src/orm/orm-session.ts +159 -159
  39. package/src/orm/relation-change-processor.ts +3 -3
  40. package/src/orm/runtime-types.ts +5 -5
  41. package/src/orm/unit-of-work.ts +13 -25
  42. package/src/query-builder/query-ast-service.ts +287 -300
  43. package/src/query-builder/relation-filter-utils.ts +159 -160
  44. package/src/query-builder/select.ts +137 -192
  45. package/src/schema/column-types.ts +4 -4
  46. package/src/schema/types.ts +5 -1
  47. package/src/core/ast/ast-validation.ts +0 -19
  48. package/src/core/ast/param-proxy.ts +0 -47
  49. package/src/core/ast/query-visitor.ts +0 -273
  50. package/src/openapi/index.ts +0 -4
  51. package/src/openapi/query-parameters.ts +0 -207
  52. package/src/openapi/schema-extractor-input.ts +0 -193
  53. package/src/openapi/schema-extractor-output.ts +0 -427
  54. package/src/openapi/schema-extractor-utils.ts +0 -110
  55. package/src/openapi/schema-extractor.ts +0 -120
  56. package/src/openapi/schema-types.ts +0 -187
  57. 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
@@ -64,7 +64,7 @@ export interface RawDefaultValue {
64
64
  raw: string;
65
65
  }
66
66
 
67
- export type DefaultValue = unknown | RawDefaultValue;
67
+ export type DefaultValue = string | number | boolean | Date | null | RawDefaultValue;
68
68
 
69
69
  export interface ForeignKeyReference {
70
70
  /** Target table name */
@@ -112,7 +112,7 @@ export interface ColumnDef<T extends ColumnType = ColumnType, TRuntime = unknown
112
112
  /** Column comment/description */
113
113
  comment?: string;
114
114
  /** Additional arguments for the column type (e.g., VARCHAR length) */
115
- args?: unknown[];
115
+ args?: (string | number)[];
116
116
  /** Table name this column belongs to (filled at runtime by defineTable) */
117
117
  table?: string;
118
118
  }
@@ -240,7 +240,7 @@ export const col = {
240
240
  * Creates a column definition with a custom SQL type.
241
241
  * Useful for dialect-specific types without polluting the standard set.
242
242
  */
243
- custom: (type: string, opts: { dialect?: string; args?: unknown[]; tsType?: unknown } = {}): ColumnDef => ({
243
+ custom: (type: string, opts: { dialect?: string; args?: (string | number)[]; tsType?: unknown } = {}): ColumnDef => ({
244
244
  name: '',
245
245
  type,
246
246
  args: opts.args,
@@ -274,7 +274,7 @@ export const col = {
274
274
  /**
275
275
  * Sets a default value for the column
276
276
  */
277
- default: <T extends ColumnType>(def: ColumnDef<T>, value: unknown): ColumnDef<T> =>
277
+ default: <T extends ColumnType>(def: ColumnDef<T>, value: DefaultValue): ColumnDef<T> =>
278
278
  ({
279
279
  ...def,
280
280
  default: value
@@ -19,6 +19,10 @@ export type RelationTargetTable<TRel extends RelationDef> =
19
19
  TRel extends BelongsToManyRelation<infer TTarget, TableDef> ? TTarget :
20
20
  never;
21
21
 
22
+ export type JsonValue = string | number | boolean | null | JsonArray | JsonObject;
23
+ export type JsonArray = Array<JsonValue>;
24
+ export interface JsonObject { [key: string]: JsonValue }
25
+
22
26
  type NormalizedColumnType<T extends ColumnDef> = Lowercase<T['type'] & string>;
23
27
 
24
28
  /**
@@ -30,7 +34,7 @@ export type ColumnToTs<T extends ColumnDef> =
30
34
  NormalizedColumnType<T> extends 'bigint' ? number | bigint :
31
35
  NormalizedColumnType<T> extends 'decimal' | 'float' | 'double' ? number :
32
36
  NormalizedColumnType<T> extends 'boolean' ? boolean :
33
- NormalizedColumnType<T> extends 'json' ? unknown :
37
+ NormalizedColumnType<T> extends 'json' ? JsonValue :
34
38
  NormalizedColumnType<T> extends 'blob' | 'binary' | 'varbinary' | 'bytea' ? Buffer :
35
39
  NormalizedColumnType<T> extends 'date' | 'datetime' | 'timestamp' | 'timestamptz' ? string :
36
40
  string
@@ -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
- };