metal-orm 1.0.77 → 1.0.78

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.77",
3
+ "version": "1.0.78",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -349,6 +349,36 @@ export const singularizeWordPtBr = (
349
349
  return normalized;
350
350
  };
351
351
 
352
+ // ═══════════════════════════════════════════════════════════════════════════
353
+ // NOUN SPECIFIERS (SUBSTANTIVOS DETERMINANTES)
354
+ // ═══════════════════════════════════════════════════════════════════════════
355
+
356
+ /**
357
+ * Portuguese words that act as specifiers/delimiters in compound nouns.
358
+ * When these appear as the second term, only the first term varies.
359
+ * @type {ReadonlySet<string>}
360
+ */
361
+ export const PT_BR_NOUN_SPECIFIERS = Object.freeze(new Set(
362
+ [
363
+ 'correcao', 'padrao', 'limite', 'chave', 'base', 'chefe',
364
+ 'satelite', 'fantasma', 'monstro', 'escola', 'piloto',
365
+ 'femea', 'macho', 'geral', 'solicitacao'
366
+ ].map(normalizeLookup)
367
+ ));
368
+
369
+ const isCompoundWithSpecifier = (term, specifiers = PT_BR_NOUN_SPECIFIERS) => {
370
+ if (!term || !String(term).trim()) return false;
371
+ const original = String(term).trim();
372
+ const format = detectTextFormat(original);
373
+ const words = splitIntoWords(original, format);
374
+
375
+ if (words.length < 2) return false;
376
+
377
+ // Check if the last word is a known specifier
378
+ const lastWord = words[words.length - 1];
379
+ return specifiers.has(normalizeLookup(lastWord));
380
+ };
381
+
352
382
  // ═══════════════════════════════════════════════════════════════════════════
353
383
  // COMPOUND TERM HANDLING
354
384
  // ═══════════════════════════════════════════════════════════════════════════
@@ -358,22 +388,26 @@ export const singularizeWordPtBr = (
358
388
  */
359
389
  export const pluralizeRelationPropertyPtBr = (
360
390
  term,
361
- { pluralizeWord = pluralizeWordPtBr, connectors = PT_BR_CONNECTORS } = {}
362
- ) =>
363
- hasConnectorWord(term, connectors)
364
- ? applyToCompoundHead(term, { connectors, transformWord: pluralizeWord })
365
- : applyToCompoundWords(term, { connectors, transformWord: pluralizeWord });
391
+ { pluralizeWord = pluralizeWordPtBr, connectors = PT_BR_CONNECTORS, specifiers = PT_BR_NOUN_SPECIFIERS } = {}
392
+ ) => {
393
+ if (hasConnectorWord(term, connectors) || isCompoundWithSpecifier(term, specifiers)) {
394
+ return applyToCompoundHead(term, { connectors, transformWord: pluralizeWord });
395
+ }
396
+ return applyToCompoundWords(term, { connectors, transformWord: pluralizeWord });
397
+ }
366
398
 
367
399
  /**
368
400
  * Singularizes a compound property/relation name in Portuguese.
369
401
  */
370
402
  export const singularizeRelationPropertyPtBr = (
371
403
  term,
372
- { singularizeWord = singularizeWordPtBr, connectors = PT_BR_CONNECTORS } = {}
373
- ) =>
374
- hasConnectorWord(term, connectors)
375
- ? applyToCompoundHead(term, { connectors, transformWord: singularizeWord })
376
- : applyToCompoundWords(term, { connectors, transformWord: singularizeWord });
404
+ { singularizeWord = singularizeWordPtBr, connectors = PT_BR_CONNECTORS, specifiers = PT_BR_NOUN_SPECIFIERS } = {}
405
+ ) => {
406
+ if (hasConnectorWord(term, connectors) || isCompoundWithSpecifier(term, specifiers)) {
407
+ return applyToCompoundHead(term, { connectors, transformWord: singularizeWord });
408
+ }
409
+ return applyToCompoundWords(term, { connectors, transformWord: singularizeWord });
410
+ }
377
411
 
378
412
  // ═══════════════════════════════════════════════════════════════════════════
379
413
  // INFLECTOR FACTORY
@@ -393,14 +427,17 @@ export const createPtBrInflector = ({ customIrregulars = {} } = {}) => {
393
427
  ...buildSingularIrregulars(customIrregulars)
394
428
  });
395
429
 
430
+ const pluralizeWord = (w) => pluralizeWordPtBr(w, irregularPlurals);
431
+ const singularizeWord = (w) => singularizeWordPtBr(w, irregularSingulars);
432
+
396
433
  return Object.freeze({
397
434
  locale: 'pt-BR',
398
435
  irregularPlurals,
399
436
  irregularSingulars,
400
- pluralizeWord: (w) => pluralizeWordPtBr(w, irregularPlurals),
401
- singularizeWord: (w) => singularizeWordPtBr(w, irregularSingulars),
402
- pluralizeRelationProperty: pluralizeRelationPropertyPtBr,
403
- singularizeRelationProperty: singularizeRelationPropertyPtBr,
437
+ pluralizeWord,
438
+ singularizeWord,
439
+ pluralizeRelationProperty: (term) => pluralizeRelationPropertyPtBr(term, { pluralizeWord }),
440
+ singularizeRelationProperty: (term) => singularizeRelationPropertyPtBr(term, { singularizeWord }),
404
441
  normalizeForLookup: normalizeWord
405
442
  });
406
443
  };
@@ -1,79 +1,86 @@
1
- import { TableDef } from '../../schema/table.js';
2
- import { ColumnDef } from '../../schema/column-types.js';
3
- import { OrderingTerm, SelectQueryNode } from '../../core/ast/query.js';
4
- import { FunctionNode, ExpressionNode, exists, notExists } from '../../core/ast/expression.js';
5
- import { derivedTable } from '../../core/ast/builders.js';
6
- import { SelectQueryState } from '../select-query-state.js';
7
- import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps.js';
8
- import { SelectPredicateFacet } from './predicate-facet.js';
9
- import { SelectRelationFacet } from './relation-facet.js';
10
- import { ORDER_DIRECTIONS, OrderDirection } from '../../core/sql/sql.js';
11
- import { OrmSession } from '../../orm/orm-session.js';
12
- import type { SelectQueryBuilder } from '../select.js';
13
-
14
- export type WhereHasOptions = {
15
- correlate?: ExpressionNode;
16
- };
17
-
18
- export type RelationCallback = <TChildTable extends TableDef>(
19
- qb: SelectQueryBuilder<unknown, TChildTable>
20
- ) => SelectQueryBuilder<unknown, TChildTable>;
21
-
22
- type ChildBuilderFactory = <R, TChild extends TableDef>(table: TChild) => SelectQueryBuilder<R, TChild>;
23
-
24
- /**
25
- * Builds a new query context with an ORDER BY clause applied.
26
- */
27
- export function applyOrderBy(
28
- context: SelectQueryBuilderContext,
29
- predicateFacet: SelectPredicateFacet,
30
- term: ColumnDef | OrderingTerm,
31
- directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string }
32
- ): SelectQueryBuilderContext {
33
- const options =
34
- typeof directionOrOptions === 'string' ? { direction: directionOrOptions } : directionOrOptions;
35
- const dir = options.direction ?? ORDER_DIRECTIONS.ASC;
36
- return predicateFacet.orderBy(context, term, dir, options.nulls, options.collation);
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { ColumnDef } from '../../schema/column-types.js';
3
+ import { OrderingTerm, SelectQueryNode } from '../../core/ast/query.js';
4
+ import { FunctionNode, ExpressionNode, exists, notExists } from '../../core/ast/expression.js';
5
+ import { derivedTable } from '../../core/ast/builders.js';
6
+ import { SelectQueryState } from '../select-query-state.js';
7
+ import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps.js';
8
+ import { SelectPredicateFacet } from './predicate-facet.js';
9
+ import { SelectRelationFacet } from './relation-facet.js';
10
+ import { ORDER_DIRECTIONS, OrderDirection } from '../../core/sql/sql.js';
11
+ import { OrmSession } from '../../orm/orm-session.js';
12
+ import type { SelectQueryBuilder } from '../select.js';
13
+
14
+ export type WhereHasOptions = {
15
+ correlate?: ExpressionNode;
16
+ };
17
+
18
+ export type RelationCallback = <TChildTable extends TableDef>(
19
+ qb: SelectQueryBuilder<unknown, TChildTable>
20
+ ) => SelectQueryBuilder<unknown, TChildTable>;
21
+
22
+ type ChildBuilderFactory = <R, TChild extends TableDef>(table: TChild) => SelectQueryBuilder<R, TChild>;
23
+
24
+ /**
25
+ * Builds a new query context with an ORDER BY clause applied.
26
+ */
27
+ export function applyOrderBy(
28
+ context: SelectQueryBuilderContext,
29
+ predicateFacet: SelectPredicateFacet,
30
+ term: ColumnDef | OrderingTerm,
31
+ directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string }
32
+ ): SelectQueryBuilderContext {
33
+ const options =
34
+ typeof directionOrOptions === 'string' ? { direction: directionOrOptions } : directionOrOptions;
35
+ const dir = options.direction ?? ORDER_DIRECTIONS.ASC;
36
+ return predicateFacet.orderBy(context, term, dir, options.nulls, options.collation);
37
+ }
38
+
39
+ /**
40
+ * Runs the count query for the provided context and session.
41
+ */
42
+ export async function executeCount(
43
+ context: SelectQueryBuilderContext,
44
+ env: SelectQueryBuilderEnvironment,
45
+ session: OrmSession
46
+ ): Promise<number> {
47
+ const unpagedAst: SelectQueryNode = {
48
+ ...context.state.ast,
49
+ orderBy: undefined,
50
+ limit: undefined,
51
+ offset: undefined
52
+ };
53
+
54
+ const nextState = new SelectQueryState(env.table as TableDef, unpagedAst);
55
+ const nextContext: SelectQueryBuilderContext = {
56
+ ...context,
57
+ state: nextState
58
+ };
59
+
60
+ const subAst = nextContext.hydration.applyToAst(nextState.ast);
61
+ const countQuery: SelectQueryNode = {
62
+ type: 'SelectQuery',
63
+ from: derivedTable(subAst, '__metal_count'),
64
+ columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
65
+ joins: []
66
+ };
67
+
68
+ const execCtx = session.getExecutionContext();
69
+ const compiled = execCtx.dialect.compileSelect(countQuery);
70
+ const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
71
+ const value = results[0]?.values?.[0]?.[0];
72
+
73
+ if (typeof value === 'number') return value;
74
+ if (typeof value === 'bigint') return Number(value);
75
+ if (typeof value === 'string') return Number(value);
76
+ return value === null || value === undefined ? 0 : Number(value);
37
77
  }
38
78
 
39
- /**
40
- * Runs the count query for the provided context and session.
41
- */
42
- export async function executeCount(
43
- context: SelectQueryBuilderContext,
44
- env: SelectQueryBuilderEnvironment,
45
- session: OrmSession
46
- ): Promise<number> {
47
- const unpagedAst: SelectQueryNode = {
48
- ...context.state.ast,
49
- orderBy: undefined,
50
- limit: undefined,
51
- offset: undefined
52
- };
53
-
54
- const nextState = new SelectQueryState(env.table as TableDef, unpagedAst);
55
- const nextContext: SelectQueryBuilderContext = {
56
- ...context,
57
- state: nextState
58
- };
59
-
60
- const subAst = nextContext.hydration.applyToAst(nextState.ast);
61
- const countQuery: SelectQueryNode = {
62
- type: 'SelectQuery',
63
- from: derivedTable(subAst, '__metal_count'),
64
- columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
65
- joins: []
66
- };
67
-
68
- const execCtx = session.getExecutionContext();
69
- const compiled = execCtx.dialect.compileSelect(countQuery);
70
- const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
71
- const value = results[0]?.values?.[0]?.[0];
72
-
73
- if (typeof value === 'number') return value;
74
- if (typeof value === 'bigint') return Number(value);
75
- if (typeof value === 'string') return Number(value);
76
- return value === null || value === undefined ? 0 : Number(value);
79
+ export interface PaginatedResult<T> {
80
+ items: T[];
81
+ totalItems: number;
82
+ page: number;
83
+ pageSize: number;
77
84
  }
78
85
 
79
86
  /**
@@ -84,7 +91,7 @@ export async function executePagedQuery<T, TTable extends TableDef>(
84
91
  session: OrmSession,
85
92
  options: { page: number; pageSize: number },
86
93
  countCallback: (session: OrmSession) => Promise<number>
87
- ): Promise<{ items: T[]; totalItems: number }> {
94
+ ): Promise<PaginatedResult<T>> {
88
95
  const { page, pageSize } = options;
89
96
 
90
97
  if (!Number.isInteger(page) || page < 1) {
@@ -101,44 +108,44 @@ export async function executePagedQuery<T, TTable extends TableDef>(
101
108
  countCallback(session)
102
109
  ]);
103
110
 
104
- return { items, totalItems };
105
- }
106
-
107
- /**
108
- * Builds an EXISTS or NOT EXISTS predicate for a related table.
109
- */
110
- export function buildWhereHasPredicate<TTable extends TableDef>(
111
- env: SelectQueryBuilderEnvironment,
112
- context: SelectQueryBuilderContext,
113
- relationFacet: SelectRelationFacet,
114
- createChildBuilder: ChildBuilderFactory,
115
- relationName: keyof TTable['relations'] & string,
116
- callbackOrOptions?: RelationCallback | WhereHasOptions,
117
- maybeOptions?: WhereHasOptions,
118
- negate = false
119
- ): ExpressionNode {
120
- const relation = env.table.relations[relationName as string];
121
- if (!relation) {
122
- throw new Error(`Relation '${relationName}' not found on table '${env.table.name}'`);
123
- }
124
-
125
- const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions : undefined;
126
- const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as
127
- | WhereHasOptions
128
- | undefined;
129
-
130
- let subQb = createChildBuilder<unknown, TableDef>(relation.target);
131
- if (callback) {
132
- subQb = callback(subQb);
133
- }
134
-
135
- const subAst = subQb.getAST();
136
- const finalSubAst = relationFacet.applyRelationCorrelation(
137
- context,
138
- relationName,
139
- subAst,
140
- options?.correlate
141
- );
142
-
143
- return negate ? notExists(finalSubAst) : exists(finalSubAst);
111
+ return { items, totalItems, page, pageSize };
144
112
  }
113
+
114
+ /**
115
+ * Builds an EXISTS or NOT EXISTS predicate for a related table.
116
+ */
117
+ export function buildWhereHasPredicate<TTable extends TableDef>(
118
+ env: SelectQueryBuilderEnvironment,
119
+ context: SelectQueryBuilderContext,
120
+ relationFacet: SelectRelationFacet,
121
+ createChildBuilder: ChildBuilderFactory,
122
+ relationName: keyof TTable['relations'] & string,
123
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
124
+ maybeOptions?: WhereHasOptions,
125
+ negate = false
126
+ ): ExpressionNode {
127
+ const relation = env.table.relations[relationName as string];
128
+ if (!relation) {
129
+ throw new Error(`Relation '${relationName}' not found on table '${env.table.name}'`);
130
+ }
131
+
132
+ const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions : undefined;
133
+ const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as
134
+ | WhereHasOptions
135
+ | undefined;
136
+
137
+ let subQb = createChildBuilder<unknown, TableDef>(relation.target);
138
+ if (callback) {
139
+ subQb = callback(subQb);
140
+ }
141
+
142
+ const subAst = subQb.getAST();
143
+ const finalSubAst = relationFacet.applyRelationCorrelation(
144
+ context,
145
+ relationName,
146
+ subAst,
147
+ options?.correlate
148
+ );
149
+
150
+ return negate ? notExists(finalSubAst) : exists(finalSubAst);
151
+ }
@@ -50,14 +50,16 @@ import { executeHydrated, executeHydratedPlain, executeHydratedWithContexts } fr
50
50
  import { EntityConstructor } from '../orm/entity-metadata.js';
51
51
  import { materializeAs } from '../orm/entity-materializer.js';
52
52
  import { resolveSelectQuery } from './query-resolution.js';
53
- import {
54
- applyOrderBy,
55
- buildWhereHasPredicate,
56
- executeCount,
57
- executePagedQuery,
58
- RelationCallback,
59
- WhereHasOptions
60
- } from './select/select-operations.js';
53
+ import {
54
+ applyOrderBy,
55
+ buildWhereHasPredicate,
56
+ executeCount,
57
+ executePagedQuery,
58
+ PaginatedResult,
59
+ RelationCallback,
60
+ WhereHasOptions
61
+ } from './select/select-operations.js';
62
+ export type { PaginatedResult };
61
63
  import { SelectFromFacet } from './select/from-facet.js';
62
64
  import { SelectJoinFacet } from './select/join-facet.js';
63
65
  import { SelectProjectionFacet } from './select/projection-facet.js';
@@ -792,19 +794,19 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
792
794
  return executeCount(this.context, this.env, session);
793
795
  }
794
796
 
795
- /**
796
- * Executes the query and returns both the paged items and the total.
797
- *
798
- * @example
799
- * const { items, totalItems } = await qb.executePaged(session, { page: 1, pageSize: 20 });
800
- */
801
- async executePaged(
802
- session: OrmSession,
803
- options: { page: number; pageSize: number }
804
- ): Promise<{ items: T[]; totalItems: number }> {
805
- const builder = this.ensureDefaultSelection();
806
- return executePagedQuery(builder, session, options, sess => this.count(sess));
807
- }
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 => this.count(sess));
809
+ }
808
810
 
809
811
  /**
810
812
  * Executes the query with provided execution and hydration contexts