@mostajs/orm 1.0.0

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +548 -0
  3. package/dist/core/base-repository.d.ts +26 -0
  4. package/dist/core/base-repository.js +82 -0
  5. package/dist/core/config.d.ts +62 -0
  6. package/dist/core/config.js +116 -0
  7. package/dist/core/errors.d.ts +30 -0
  8. package/dist/core/errors.js +49 -0
  9. package/dist/core/factory.d.ts +41 -0
  10. package/dist/core/factory.js +142 -0
  11. package/dist/core/normalizer.d.ts +9 -0
  12. package/dist/core/normalizer.js +19 -0
  13. package/dist/core/registry.d.ts +43 -0
  14. package/dist/core/registry.js +78 -0
  15. package/dist/core/types.d.ts +228 -0
  16. package/dist/core/types.js +5 -0
  17. package/dist/dialects/abstract-sql.dialect.d.ts +113 -0
  18. package/dist/dialects/abstract-sql.dialect.js +1071 -0
  19. package/dist/dialects/cockroachdb.dialect.d.ts +2 -0
  20. package/dist/dialects/cockroachdb.dialect.js +23 -0
  21. package/dist/dialects/db2.dialect.d.ts +2 -0
  22. package/dist/dialects/db2.dialect.js +190 -0
  23. package/dist/dialects/hana.dialect.d.ts +2 -0
  24. package/dist/dialects/hana.dialect.js +199 -0
  25. package/dist/dialects/hsqldb.dialect.d.ts +2 -0
  26. package/dist/dialects/hsqldb.dialect.js +114 -0
  27. package/dist/dialects/mariadb.dialect.d.ts +2 -0
  28. package/dist/dialects/mariadb.dialect.js +87 -0
  29. package/dist/dialects/mongo.dialect.d.ts +2 -0
  30. package/dist/dialects/mongo.dialect.js +480 -0
  31. package/dist/dialects/mssql.dialect.d.ts +27 -0
  32. package/dist/dialects/mssql.dialect.js +127 -0
  33. package/dist/dialects/mysql.dialect.d.ts +24 -0
  34. package/dist/dialects/mysql.dialect.js +101 -0
  35. package/dist/dialects/oracle.dialect.d.ts +2 -0
  36. package/dist/dialects/oracle.dialect.js +206 -0
  37. package/dist/dialects/postgres.dialect.d.ts +26 -0
  38. package/dist/dialects/postgres.dialect.js +105 -0
  39. package/dist/dialects/spanner.dialect.d.ts +2 -0
  40. package/dist/dialects/spanner.dialect.js +259 -0
  41. package/dist/dialects/sqlite.dialect.d.ts +2 -0
  42. package/dist/dialects/sqlite.dialect.js +1027 -0
  43. package/dist/dialects/sybase.dialect.d.ts +2 -0
  44. package/dist/dialects/sybase.dialect.js +119 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +26 -0
  47. package/docs/api-reference.md +1009 -0
  48. package/docs/dialects.md +673 -0
  49. package/docs/tutorial.md +846 -0
  50. package/package.json +91 -0
@@ -0,0 +1,228 @@
1
+ export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array';
2
+ export interface FieldDef {
3
+ type: FieldType;
4
+ required?: boolean;
5
+ unique?: boolean;
6
+ sparse?: boolean;
7
+ default?: unknown;
8
+ enum?: string[];
9
+ lowercase?: boolean;
10
+ trim?: boolean;
11
+ arrayOf?: FieldType | EmbeddedSchemaDef;
12
+ }
13
+ /** Embedded sub-document (e.g. Activity.schedule[], SubscriptionPlan.activities[]) */
14
+ export interface EmbeddedSchemaDef {
15
+ kind: 'embedded';
16
+ fields: Record<string, FieldDef>;
17
+ }
18
+ export type RelationType = 'one-to-one' | 'many-to-one' | 'one-to-many' | 'many-to-many';
19
+ export interface RelationDef {
20
+ /** Target entity name (e.g. 'User', 'Client') */
21
+ target: string;
22
+ type: RelationType;
23
+ required?: boolean;
24
+ /** Default fields to select when populating/joining */
25
+ select?: string[];
26
+ /** Whether this relation can be null */
27
+ nullable?: boolean;
28
+ /** Junction table name (SQL dialects) — convention: "{source}_{target}" in snake_case */
29
+ through?: string;
30
+ }
31
+ export type IndexType = 'asc' | 'desc' | 'text';
32
+ export interface IndexDef {
33
+ fields: Record<string, IndexType>;
34
+ unique?: boolean;
35
+ sparse?: boolean;
36
+ }
37
+ export interface EntitySchema {
38
+ /** Entity name (PascalCase, e.g. 'Client') */
39
+ name: string;
40
+ /** Collection/table name (e.g. 'clients') */
41
+ collection: string;
42
+ /** Field definitions */
43
+ fields: Record<string, FieldDef>;
44
+ /** Relations to other entities */
45
+ relations: Record<string, RelationDef>;
46
+ /** Database indexes */
47
+ indexes: IndexDef[];
48
+ /** Auto-manage createdAt/updatedAt */
49
+ timestamps: boolean;
50
+ }
51
+ export interface FilterOperator {
52
+ $eq?: unknown;
53
+ $ne?: unknown;
54
+ $gt?: unknown;
55
+ $gte?: unknown;
56
+ $lt?: unknown;
57
+ $lte?: unknown;
58
+ $in?: unknown[];
59
+ $nin?: unknown[];
60
+ $regex?: string;
61
+ $regexFlags?: string;
62
+ $exists?: boolean;
63
+ }
64
+ export type FilterValue = unknown | FilterOperator;
65
+ export interface FilterQuery {
66
+ [field: string]: FilterValue;
67
+ $or?: FilterQuery[];
68
+ $and?: FilterQuery[];
69
+ }
70
+ export type SortDirection = 1 | -1;
71
+ export interface QueryOptions {
72
+ sort?: Record<string, SortDirection>;
73
+ skip?: number;
74
+ limit?: number;
75
+ /** Fields to include in result (projection) */
76
+ select?: string[];
77
+ /** Fields to exclude from result */
78
+ exclude?: string[];
79
+ }
80
+ export interface PaginatedResult<T> {
81
+ data: T[];
82
+ total: number;
83
+ page: number;
84
+ limit: number;
85
+ totalPages: number;
86
+ }
87
+ export interface AggregateGroupStage {
88
+ $group: {
89
+ _by: string | null;
90
+ [field: string]: AggregateAccumulator | string | null;
91
+ };
92
+ }
93
+ export interface AggregateAccumulator {
94
+ $sum?: number | string;
95
+ $count?: true;
96
+ $avg?: string;
97
+ $min?: string;
98
+ $max?: string;
99
+ }
100
+ export interface AggregateMatchStage {
101
+ $match: FilterQuery;
102
+ }
103
+ export interface AggregateSortStage {
104
+ $sort: Record<string, SortDirection>;
105
+ }
106
+ export interface AggregateLimitStage {
107
+ $limit: number;
108
+ }
109
+ export type AggregateStage = AggregateMatchStage | AggregateGroupStage | AggregateSortStage | AggregateLimitStage;
110
+ export type DialectType = 'mongodb' | 'sqlite' | 'postgres' | 'mysql' | 'mariadb' | 'oracle' | 'mssql' | 'cockroachdb' | 'db2' | 'hana' | 'hsqldb' | 'spanner' | 'sybase';
111
+ /**
112
+ * Schema generation strategy (inspired by hibernate.hbm2ddl.auto)
113
+ *
114
+ * validate : validate schema, make no changes (production)
115
+ * update : update schema to match entities (dev)
116
+ * create : drop and recreate schema on startup
117
+ * create-drop : drop schema on shutdown
118
+ * none : do nothing
119
+ */
120
+ export type SchemaStrategy = 'validate' | 'update' | 'create' | 'create-drop' | 'none';
121
+ /**
122
+ * Connection configuration — inspired by Hibernate persistence.xml
123
+ *
124
+ * Equivalent persistence.xml properties :
125
+ * jakarta.persistence.jdbc.url → uri
126
+ * hibernate.dialect → dialect
127
+ * hibernate.show_sql → showSql
128
+ * hibernate.format_sql → formatSql
129
+ * hibernate.hbm2ddl.auto → schemaStrategy
130
+ * hibernate.connection.pool_size → poolSize
131
+ * hibernate.cache.use_second_level → cacheEnabled
132
+ * hibernate.default_batch_fetch_size → batchSize
133
+ */
134
+ export interface ConnectionConfig {
135
+ dialect: DialectType;
136
+ uri: string;
137
+ /** Log generated queries to console (default: false) */
138
+ showSql?: boolean;
139
+ /** Pretty-print logged queries (default: false) */
140
+ formatSql?: boolean;
141
+ /** Schema generation strategy (default: 'none') */
142
+ schemaStrategy?: SchemaStrategy;
143
+ /** Max connections in pool (default: dialect-specific) */
144
+ poolSize?: number;
145
+ /** Enable query result caching (default: false) */
146
+ cacheEnabled?: boolean;
147
+ /** Cache TTL in seconds (default: 60) */
148
+ cacheTtlSeconds?: number;
149
+ /** Default batch size for bulk operations (default: 25) */
150
+ batchSize?: number;
151
+ /** Additional dialect-specific options */
152
+ options?: Record<string, unknown>;
153
+ }
154
+ export interface IDialect {
155
+ readonly dialectType: DialectType;
156
+ connect(config: ConnectionConfig): Promise<void>;
157
+ disconnect(): Promise<void>;
158
+ testConnection(): Promise<boolean>;
159
+ initSchema(schemas: EntitySchema[]): Promise<void>;
160
+ find<T = Record<string, unknown>>(schema: EntitySchema, filter: FilterQuery, options?: QueryOptions): Promise<T[]>;
161
+ findOne<T = Record<string, unknown>>(schema: EntitySchema, filter: FilterQuery, options?: QueryOptions): Promise<T | null>;
162
+ findById<T = Record<string, unknown>>(schema: EntitySchema, id: string, options?: QueryOptions): Promise<T | null>;
163
+ create<T = Record<string, unknown>>(schema: EntitySchema, data: Record<string, unknown>): Promise<T>;
164
+ update<T = Record<string, unknown>>(schema: EntitySchema, id: string, data: Record<string, unknown>): Promise<T | null>;
165
+ updateMany(schema: EntitySchema, filter: FilterQuery, data: Record<string, unknown>): Promise<number>;
166
+ delete(schema: EntitySchema, id: string): Promise<boolean>;
167
+ deleteMany(schema: EntitySchema, filter: FilterQuery): Promise<number>;
168
+ count(schema: EntitySchema, filter: FilterQuery): Promise<number>;
169
+ distinct(schema: EntitySchema, field: string, filter: FilterQuery): Promise<unknown[]>;
170
+ aggregate<T = Record<string, unknown>>(schema: EntitySchema, stages: AggregateStage[]): Promise<T[]>;
171
+ findWithRelations<T = Record<string, unknown>>(schema: EntitySchema, filter: FilterQuery, relations: string[], options?: QueryOptions): Promise<T[]>;
172
+ findByIdWithRelations<T = Record<string, unknown>>(schema: EntitySchema, id: string, relations: string[], options?: QueryOptions): Promise<T | null>;
173
+ upsert<T = Record<string, unknown>>(schema: EntitySchema, filter: FilterQuery, data: Record<string, unknown>): Promise<T>;
174
+ increment(schema: EntitySchema, id: string, field: string, amount: number): Promise<Record<string, unknown>>;
175
+ addToSet(schema: EntitySchema, id: string, field: string, value: unknown): Promise<Record<string, unknown> | null>;
176
+ pull(schema: EntitySchema, id: string, field: string, value: unknown): Promise<Record<string, unknown> | null>;
177
+ search<T = Record<string, unknown>>(schema: EntitySchema, query: string, fields: string[], options?: QueryOptions): Promise<T[]>;
178
+ }
179
+ export interface IRepository<T> {
180
+ findAll(filter?: FilterQuery, options?: QueryOptions): Promise<T[]>;
181
+ findOne(filter: FilterQuery, options?: QueryOptions): Promise<T | null>;
182
+ findById(id: string, options?: QueryOptions): Promise<T | null>;
183
+ findByIdWithRelations(id: string, relations?: string[], options?: QueryOptions): Promise<T | null>;
184
+ create(data: Partial<T>): Promise<T>;
185
+ update(id: string, data: Partial<T>): Promise<T | null>;
186
+ updateMany(filter: FilterQuery, data: Partial<T>): Promise<number>;
187
+ delete(id: string): Promise<boolean>;
188
+ deleteMany(filter: FilterQuery): Promise<number>;
189
+ count(filter?: FilterQuery): Promise<number>;
190
+ search(query: string, options?: QueryOptions): Promise<T[]>;
191
+ distinct(field: string, filter?: FilterQuery): Promise<unknown[]>;
192
+ aggregate<R = Record<string, unknown>>(stages: AggregateStage[]): Promise<R[]>;
193
+ upsert(filter: FilterQuery, data: Partial<T>): Promise<T>;
194
+ increment(id: string, field: string, amount: number): Promise<T | null>;
195
+ addToSet(id: string, field: string, value: unknown): Promise<T | null>;
196
+ pull(id: string, field: string, value: unknown): Promise<T | null>;
197
+ findWithRelations(filter: FilterQuery, relations: string[], options?: QueryOptions): Promise<T[]>;
198
+ }
199
+ export interface HookContext {
200
+ entity: EntitySchema;
201
+ dialect: DialectType;
202
+ operation: 'create' | 'update' | 'delete' | 'find';
203
+ userId?: string;
204
+ }
205
+ export interface IPlugin {
206
+ name: string;
207
+ /** Modify the schema at boot time (add fields, indexes...) */
208
+ onSchemaInit?(schema: EntitySchema): EntitySchema;
209
+ /** Before insert */
210
+ preSave?(doc: Record<string, unknown>, ctx: HookContext): Promise<Record<string, unknown>> | Record<string, unknown>;
211
+ /** After insert */
212
+ postSave?(doc: Record<string, unknown>, ctx: HookContext): Promise<void> | void;
213
+ /** Before update */
214
+ preUpdate?(id: string, data: Record<string, unknown>, ctx: HookContext): Promise<Record<string, unknown>> | Record<string, unknown>;
215
+ /** After update */
216
+ postUpdate?(doc: Record<string, unknown>, ctx: HookContext): Promise<void> | void;
217
+ /** Before delete */
218
+ preDelete?(id: string, ctx: HookContext): Promise<void> | void;
219
+ /** Transform queries (e.g. soft-delete auto-filter) */
220
+ onQuery?(filter: FilterQuery, ctx: HookContext): FilterQuery;
221
+ /** Transform results (e.g. normalization) */
222
+ onResult?(doc: Record<string, unknown>, ctx: HookContext): Record<string, unknown>;
223
+ }
224
+ /** Normalized document: id (string) instead of _id */
225
+ export interface NormalizedDoc {
226
+ id: string;
227
+ [key: string]: unknown;
228
+ }
@@ -0,0 +1,5 @@
1
+ // DAL Core Types - Database Abstraction Layer
2
+ // Inspired by Hibernate ORM Dialect pattern
3
+ // Zero dependency on any specific database driver
4
+ // Author: Dr Hamid MADANI drmdh@msn.com
5
+ export {};
@@ -0,0 +1,113 @@
1
+ import type { IDialect, DialectType, ConnectionConfig, EntitySchema, FieldDef, FilterQuery as DALFilter, QueryOptions, AggregateStage } from '../core/types.js';
2
+ interface WhereClause {
3
+ sql: string;
4
+ params: unknown[];
5
+ }
6
+ export declare abstract class AbstractSqlDialect implements IDialect {
7
+ abstract readonly dialectType: DialectType;
8
+ /** Quote an identifier (column/table name) for this dialect */
9
+ abstract quoteIdentifier(name: string): string;
10
+ /** Get the parameter placeholder for index (1-based). E.g. $1, ?, :1, @p1 */
11
+ abstract getPlaceholder(index: number): string;
12
+ /** Map a DAL FieldDef to the native SQL column type */
13
+ abstract fieldToSqlType(field: FieldDef): string;
14
+ /** Get the SQL column type for the primary key (id) column */
15
+ abstract getIdColumnType(): string;
16
+ /** Execute a SELECT query, return rows */
17
+ abstract executeQuery<T>(sql: string, params: unknown[]): Promise<T[]>;
18
+ /** Execute a non-SELECT statement (INSERT/UPDATE/DELETE), return changes count */
19
+ abstract executeRun(sql: string, params: unknown[]): Promise<{
20
+ changes: number;
21
+ }>;
22
+ /** Establish the actual database connection */
23
+ abstract doConnect(config: ConnectionConfig): Promise<void>;
24
+ /** Close the actual database connection */
25
+ abstract doDisconnect(): Promise<void>;
26
+ /** Test the connection is alive */
27
+ abstract doTestConnection(): Promise<boolean>;
28
+ /** Return a SQL query that lists table names. Result rows must have a 'name' column. */
29
+ abstract getTableListQuery(): string;
30
+ protected config: ConnectionConfig | null;
31
+ protected schemas: EntitySchema[];
32
+ protected showSql: boolean;
33
+ protected formatSql: boolean;
34
+ private paramCounter;
35
+ /** Whether this dialect supports CREATE TABLE IF NOT EXISTS */
36
+ protected supportsIfNotExists(): boolean;
37
+ /** Whether this dialect supports RETURNING clause on INSERT */
38
+ protected supportsReturning(): boolean;
39
+ /** Serialize a JS boolean to a DB value (default: 1/0) */
40
+ protected serializeBoolean(v: boolean): unknown;
41
+ /** Deserialize a DB value to a JS boolean */
42
+ protected deserializeBoolean(v: unknown): boolean;
43
+ /** Build the LIMIT/OFFSET clause (dialect-specific override) */
44
+ protected buildLimitOffset(options?: QueryOptions): string;
45
+ /** Get the CREATE TABLE prefix, including IF NOT EXISTS when supported */
46
+ protected getCreateTablePrefix(tableName: string): string;
47
+ /** Get the CREATE INDEX prefix, including IF NOT EXISTS when supported */
48
+ protected getCreateIndexPrefix(indexName: string, unique: boolean): string;
49
+ /** Serialize date values to a format suitable for this dialect */
50
+ protected serializeDate(value: unknown): unknown;
51
+ /** Serialize JSON/array values */
52
+ protected serializeJson(value: unknown): unknown;
53
+ /**
54
+ * Build a regex/LIKE condition. Override for case-sensitive dialects.
55
+ * Default: LIKE (case-insensitive in MySQL, SQLite, MSSQL; case-sensitive in Postgres, Oracle, DB2, HANA).
56
+ * Postgres overrides to use ILIKE when flags contain 'i'.
57
+ */
58
+ protected buildRegexCondition(col: string, flags?: string): string;
59
+ /** Dialect label for logging */
60
+ protected getDialectLabel(): string;
61
+ protected log(operation: string, table: string, details?: unknown): void;
62
+ /** Reset the parameter counter (call before building a new statement) */
63
+ protected resetParams(): void;
64
+ /** Get the next placeholder and increment the counter */
65
+ protected nextPlaceholder(): string;
66
+ protected serializeValue(value: unknown, field?: FieldDef): unknown;
67
+ protected deserializeRow(row: Record<string, unknown>, schema: EntitySchema): Record<string, unknown>;
68
+ protected deserializeField(val: unknown, field: FieldDef): unknown;
69
+ protected translateFilter(filter: DALFilter, schema: EntitySchema): WhereClause;
70
+ protected serializeForFilter(value: unknown, fieldName: string, schema: EntitySchema): unknown;
71
+ protected buildSelectColumns(schema: EntitySchema, options?: QueryOptions): string;
72
+ protected getAllColumns(schema: EntitySchema): string[];
73
+ protected buildOrderBy(options?: QueryOptions): string;
74
+ protected prepareInsertData(schema: EntitySchema, data: Record<string, unknown>): {
75
+ columns: string[];
76
+ placeholders: string[];
77
+ values: unknown[];
78
+ };
79
+ protected prepareUpdateData(schema: EntitySchema, data: Record<string, unknown>): {
80
+ setClauses: string[];
81
+ values: unknown[];
82
+ };
83
+ protected generateCreateTable(schema: EntitySchema): string;
84
+ protected generateIndexes(schema: EntitySchema): string[];
85
+ connect(config: ConnectionConfig): Promise<void>;
86
+ disconnect(): Promise<void>;
87
+ testConnection(): Promise<boolean>;
88
+ initSchema(schemas: EntitySchema[]): Promise<void>;
89
+ find<T>(schema: EntitySchema, filter: DALFilter, options?: QueryOptions): Promise<T[]>;
90
+ findOne<T>(schema: EntitySchema, filter: DALFilter, options?: QueryOptions): Promise<T | null>;
91
+ findById<T>(schema: EntitySchema, id: string, options?: QueryOptions): Promise<T | null>;
92
+ create<T>(schema: EntitySchema, data: Record<string, unknown>): Promise<T>;
93
+ update<T>(schema: EntitySchema, id: string, data: Record<string, unknown>): Promise<T | null>;
94
+ updateMany(schema: EntitySchema, filter: DALFilter, data: Record<string, unknown>): Promise<number>;
95
+ delete(schema: EntitySchema, id: string): Promise<boolean>;
96
+ deleteMany(schema: EntitySchema, filter: DALFilter): Promise<number>;
97
+ count(schema: EntitySchema, filter: DALFilter): Promise<number>;
98
+ distinct(schema: EntitySchema, field: string, filter: DALFilter): Promise<unknown[]>;
99
+ aggregate<T>(schema: EntitySchema, stages: AggregateStage[]): Promise<T[]>;
100
+ findWithRelations<T>(schema: EntitySchema, filter: DALFilter, relations: string[], options?: QueryOptions): Promise<T[]>;
101
+ findByIdWithRelations<T>(schema: EntitySchema, id: string, relations: string[], options?: QueryOptions): Promise<T | null>;
102
+ private populateRelations;
103
+ upsert<T>(schema: EntitySchema, filter: DALFilter, data: Record<string, unknown>): Promise<T>;
104
+ increment(schema: EntitySchema, id: string, field: string, amount: number): Promise<Record<string, unknown>>;
105
+ addToSet(schema: EntitySchema, id: string, field: string, value: unknown): Promise<Record<string, unknown> | null>;
106
+ pull(schema: EntitySchema, id: string, field: string, value: unknown): Promise<Record<string, unknown> | null>;
107
+ search<T>(schema: EntitySchema, query: string, fields: string[], options?: QueryOptions): Promise<T[]>;
108
+ /** Check if a table exists */
109
+ protected tableExists(tableName: string): Promise<boolean>;
110
+ /** Drop all tables (used by 'create' and 'create-drop' strategies) */
111
+ protected dropAllTables(): Promise<void>;
112
+ }
113
+ export {};