@promakeai/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.
package/src/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @promakeai/orm - Database-Agnostic ORM
3
+ *
4
+ * Works in browser and Node.js with any adapter implementing IDataAdapter.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { ORM, defineSchema, f } from '@promakeai/orm';
9
+ *
10
+ * // Define schema
11
+ * const schema = defineSchema({
12
+ * languages: ['en', 'tr'],
13
+ * tables: {
14
+ * products: {
15
+ * id: f.id(),
16
+ * name: f.string().translatable().required(),
17
+ * price: f.decimal().required(),
18
+ * categoryId: f.int().ref('categories'),
19
+ * },
20
+ * categories: {
21
+ * id: f.id(),
22
+ * name: f.string().translatable().required(),
23
+ * },
24
+ * },
25
+ * });
26
+ *
27
+ * // Use with adapter
28
+ * const db = new ORM(adapter, { schema });
29
+ * const products = await db.list('products', {
30
+ * where: { price: { $gt: 100 } },
31
+ * populate: ['categoryId'],
32
+ * });
33
+ * ```
34
+ */
35
+
36
+ // Main ORM Class
37
+ export { ORM } from "./ORM";
38
+
39
+ // Adapter Interface
40
+ export type { IDataAdapter } from "./adapters/IDataAdapter";
41
+
42
+ // Schema API
43
+ export {
44
+ defineSchema,
45
+ f,
46
+ mergeSchemas,
47
+ createSchemaUnsafe,
48
+ } from "./schema";
49
+ export type { FieldBuilder } from "./schema";
50
+
51
+ // Schema Validation
52
+ export {
53
+ validateSchema,
54
+ assertValidSchema,
55
+ isValidSchema,
56
+ validateTable,
57
+ ValidationErrorCode,
58
+ } from "./schema";
59
+ export type { ValidationError } from "./schema";
60
+
61
+ // String Helpers
62
+ export {
63
+ singularize,
64
+ pluralize,
65
+ toPascalCase,
66
+ toCamelCase,
67
+ toSnakeCase,
68
+ toInterfaceName,
69
+ toDbInterfaceName,
70
+ toPascalCasePlural,
71
+ toTranslationTableName,
72
+ toTranslationFKName,
73
+ } from "./schema";
74
+
75
+ // Schema Helpers
76
+ export {
77
+ getTranslatableFields,
78
+ getNonTranslatableFields,
79
+ getInsertableFields,
80
+ hasTranslatableFields,
81
+ getPrimaryKeyField,
82
+ getReferenceFields,
83
+ getMainTableFields,
84
+ getTranslationTableFields,
85
+ isRequiredField,
86
+ getRequiredFields,
87
+ getRefTarget,
88
+ getRefTargetFull,
89
+ } from "./schema";
90
+
91
+ // Query Builder
92
+ export { buildWhereClause } from "./utils/whereBuilder";
93
+ export type { WhereResult } from "./utils/whereBuilder";
94
+
95
+ // Translation Query Builder
96
+ export {
97
+ buildTranslationQuery,
98
+ buildTranslationQueryById,
99
+ buildTranslationInsert,
100
+ buildTranslationUpsert,
101
+ extractTranslatableData,
102
+ } from "./utils/translationQuery";
103
+ export type {
104
+ TranslationQueryOptions,
105
+ TranslationQueryResult,
106
+ } from "./utils/translationQuery";
107
+
108
+ // Populate Resolver
109
+ export {
110
+ resolvePopulate,
111
+ getPopulatableFields,
112
+ validatePopulate,
113
+ } from "./utils/populateResolver";
114
+ export type { PopulateAdapter } from "./utils/populateResolver";
115
+
116
+ // JSON Schema Converter
117
+ export { parseJSONSchema } from "./utils/jsonConverter";
118
+
119
+ // Types
120
+ export type {
121
+ // Field types
122
+ FieldType,
123
+ FieldDefinition,
124
+ FieldReference,
125
+ FieldBuilderLike,
126
+
127
+ // Schema types
128
+ TableDefinition,
129
+ LanguageConfig,
130
+ SchemaDefinition,
131
+ SchemaInput,
132
+
133
+ // JSON Schema types (AI-friendly)
134
+ JSONFieldDefinition,
135
+ JSONTableDefinition,
136
+ JSONSchemaDefinition,
137
+
138
+ // Query types
139
+ WhereCondition,
140
+ OrderByOption,
141
+ PopulateOption,
142
+ PopulateNested,
143
+ QueryOptions,
144
+ PaginatedResult,
145
+
146
+ // Config
147
+ ORMConfig,
148
+ } from "./types";
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Schema Definition API
3
+ *
4
+ * Main entry point for defining database schemas.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { defineSchema, f } from '@promakeai/orm';
9
+ *
10
+ * export default defineSchema({
11
+ * languages: ['en', 'tr', 'de'],
12
+ * tables: {
13
+ * products: {
14
+ * id: f.id(),
15
+ * price: f.decimal().required(),
16
+ * name: f.string().translatable().required(),
17
+ * category_id: f.int().ref('categories'),
18
+ * },
19
+ * categories: {
20
+ * id: f.id(),
21
+ * slug: f.string().required().unique(),
22
+ * name: f.string().translatable().required(),
23
+ * },
24
+ * },
25
+ * });
26
+ * ```
27
+ */
28
+
29
+ import type {
30
+ SchemaDefinition,
31
+ SchemaInput,
32
+ TableDefinition,
33
+ LanguageConfig,
34
+ FieldBuilderLike,
35
+ } from "../types";
36
+ import { assertValidSchema } from "./validator";
37
+
38
+ // Re-export f for convenience
39
+ export { f } from "./fieldBuilder";
40
+
41
+ /**
42
+ * Define a database schema with type-safe DSL
43
+ *
44
+ * @param input - Schema definition with languages and tables
45
+ * @returns Validated SchemaDefinition
46
+ * @throws Error if schema is invalid
47
+ */
48
+ export function defineSchema(input: SchemaInput): SchemaDefinition {
49
+ // 1. Normalize languages
50
+ const languages = normalizeLanguages(input.languages);
51
+
52
+ // 2. Build each table's fields from DSL
53
+ const tables: Record<string, TableDefinition> = {};
54
+
55
+ for (const [tableName, fields] of Object.entries(input.tables)) {
56
+ tables[tableName] = {
57
+ name: tableName,
58
+ fields: Object.fromEntries(
59
+ Object.entries(fields).map(([fieldName, builder]) => [
60
+ fieldName,
61
+ (builder as FieldBuilderLike).build(),
62
+ ])
63
+ ),
64
+ };
65
+ }
66
+
67
+ // 3. Create schema definition
68
+ const schema: SchemaDefinition = {
69
+ name: input.name,
70
+ languages,
71
+ tables,
72
+ };
73
+
74
+ // 4. Validate
75
+ assertValidSchema(schema);
76
+
77
+ return schema;
78
+ }
79
+
80
+ /**
81
+ * Normalize language input to LanguageConfig
82
+ */
83
+ function normalizeLanguages(input: string[] | LanguageConfig): LanguageConfig {
84
+ if (Array.isArray(input)) {
85
+ if (input.length === 0) {
86
+ throw new Error("At least one language must be defined");
87
+ }
88
+ return {
89
+ default: input[0],
90
+ supported: input,
91
+ };
92
+ }
93
+ return input;
94
+ }
95
+
96
+ /**
97
+ * Create schema without validation (for testing/internal use)
98
+ * @internal
99
+ */
100
+ export function createSchemaUnsafe(input: SchemaInput): SchemaDefinition {
101
+ const languages = normalizeLanguages(input.languages);
102
+
103
+ const tables: Record<string, TableDefinition> = {};
104
+ for (const [tableName, fields] of Object.entries(input.tables)) {
105
+ tables[tableName] = {
106
+ name: tableName,
107
+ fields: Object.fromEntries(
108
+ Object.entries(fields).map(([fieldName, builder]) => [
109
+ fieldName,
110
+ (builder as FieldBuilderLike).build(),
111
+ ])
112
+ ),
113
+ };
114
+ }
115
+
116
+ return {
117
+ name: input.name,
118
+ languages,
119
+ tables,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Merge multiple schemas into one
125
+ *
126
+ * @param schemas - Array of schema definitions to merge
127
+ * @returns Merged schema definition
128
+ * @throws Error if table names conflict
129
+ */
130
+ export function mergeSchemas(schemas: SchemaDefinition[]): SchemaDefinition {
131
+ if (schemas.length === 0) {
132
+ throw new Error("At least one schema is required");
133
+ }
134
+
135
+ // Use first schema's default language
136
+ const defaultLang = schemas[0].languages.default;
137
+
138
+ // Collect all languages (unique)
139
+ const allLangs = new Set<string>();
140
+ for (const schema of schemas) {
141
+ for (const lang of schema.languages.supported) {
142
+ allLangs.add(lang);
143
+ }
144
+ }
145
+
146
+ // Merge tables
147
+ const tables: Record<string, TableDefinition> = {};
148
+ for (const schema of schemas) {
149
+ for (const [tableName, table] of Object.entries(schema.tables)) {
150
+ if (tables[tableName]) {
151
+ throw new Error(`Duplicate table name across schemas: ${tableName}`);
152
+ }
153
+ tables[tableName] = table;
154
+ }
155
+ }
156
+
157
+ return {
158
+ languages: {
159
+ default: defaultLang,
160
+ supported: Array.from(allLangs),
161
+ },
162
+ tables,
163
+ };
164
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Field Builder DSL
3
+ *
4
+ * Type-safe fluent API for field definitions.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { f } from '@promakeai/orm';
9
+ *
10
+ * const schema = defineSchema({
11
+ * tables: {
12
+ * products: {
13
+ * id: f.id(),
14
+ * price: f.decimal().required().min(0),
15
+ * name: f.string().translatable().required().trim(),
16
+ * email: f.string().lowercase().trim().unique(),
17
+ * categoryId: f.int().ref('categories'),
18
+ * brandId: f.int().ref({ table: 'brands', onDelete: 'SET_NULL' }),
19
+ * },
20
+ * },
21
+ * });
22
+ * ```
23
+ */
24
+
25
+ import type { FieldDefinition, FieldType, FieldReference } from "../types";
26
+
27
+ /**
28
+ * Fluent builder for field definitions
29
+ */
30
+ export class FieldBuilder {
31
+ protected definition: FieldDefinition;
32
+
33
+ constructor(type: FieldType) {
34
+ this.definition = {
35
+ type,
36
+ nullable: true, // Default nullable
37
+ unique: false,
38
+ primary: false,
39
+ translatable: false,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Mark field as NOT NULL
45
+ */
46
+ required(): this {
47
+ this.definition.nullable = false;
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * Mark field as nullable (default)
53
+ */
54
+ nullable(): this {
55
+ this.definition.nullable = true;
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Mark field as UNIQUE
61
+ */
62
+ unique(): this {
63
+ this.definition.unique = true;
64
+ return this;
65
+ }
66
+
67
+ /**
68
+ * Set default value
69
+ */
70
+ default(value: unknown): this {
71
+ this.definition.default = value;
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Mark field as translatable (stored in translation table)
77
+ * Only valid for string/text fields
78
+ */
79
+ translatable(): this {
80
+ this.definition.translatable = true;
81
+ return this;
82
+ }
83
+
84
+ /**
85
+ * Add reference to another table (MongoDB-style)
86
+ * @param tableOrOptions - Table name or options object
87
+ */
88
+ ref(
89
+ tableOrOptions:
90
+ | string
91
+ | {
92
+ table: string;
93
+ field?: string;
94
+ onDelete?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
95
+ onUpdate?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
96
+ }
97
+ ): this {
98
+ this.definition.ref = tableOrOptions;
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * Trim whitespace from string values
104
+ */
105
+ trim(): this {
106
+ this.definition.trim = true;
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Convert string to lowercase
112
+ */
113
+ lowercase(): this {
114
+ this.definition.lowercase = true;
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Convert string to uppercase
120
+ */
121
+ uppercase(): this {
122
+ this.definition.uppercase = true;
123
+ return this;
124
+ }
125
+
126
+ /**
127
+ * Minimum string length
128
+ */
129
+ minLength(value: number): this {
130
+ this.definition.minLength = value;
131
+ return this;
132
+ }
133
+
134
+ /**
135
+ * Maximum string length
136
+ */
137
+ maxLength(value: number): this {
138
+ this.definition.maxLength = value;
139
+ return this;
140
+ }
141
+
142
+ /**
143
+ * Minimum numeric value
144
+ */
145
+ min(value: number): this {
146
+ this.definition.min = value;
147
+ return this;
148
+ }
149
+
150
+ /**
151
+ * Maximum numeric value
152
+ */
153
+ max(value: number): this {
154
+ this.definition.max = value;
155
+ return this;
156
+ }
157
+
158
+ /**
159
+ * Enum values (allowed values)
160
+ */
161
+ enum(values: string[]): this {
162
+ this.definition.enum = values;
163
+ return this;
164
+ }
165
+
166
+ /**
167
+ * RegExp pattern to match
168
+ */
169
+ match(pattern: string): this {
170
+ this.definition.match = pattern;
171
+ return this;
172
+ }
173
+
174
+ /**
175
+ * Build the final field definition
176
+ * @internal
177
+ */
178
+ build(): FieldDefinition {
179
+ return { ...this.definition };
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Factory object for creating field builders
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * f.id() // Primary key
189
+ * f.string().required().trim() // NOT NULL trimmed string
190
+ * f.int().default(0).min(0) // Non-negative integer
191
+ * f.string().unique().lowercase() // Unique lowercase string
192
+ * f.string().translatable().required() // Translatable NOT NULL
193
+ * f.int().ref('users') // Foreign key to users
194
+ * f.int().ref({ table: 'users', onDelete: 'CASCADE' }) // With constraint
195
+ * f.json().ref('categories') // Array of category IDs
196
+ * ```
197
+ */
198
+ export const f = {
199
+ /**
200
+ * Primary key field (INTEGER PRIMARY KEY AUTOINCREMENT)
201
+ */
202
+ id: (): FieldBuilder => {
203
+ const builder = new FieldBuilder("id");
204
+ builder["definition"].primary = true;
205
+ builder["definition"].nullable = false;
206
+ return builder;
207
+ },
208
+
209
+ /**
210
+ * String field (VARCHAR/TEXT)
211
+ */
212
+ string: (): FieldBuilder => new FieldBuilder("string"),
213
+
214
+ /**
215
+ * Text field (TEXT/LONGTEXT)
216
+ */
217
+ text: (): FieldBuilder => new FieldBuilder("text"),
218
+
219
+ /**
220
+ * Integer field (INTEGER)
221
+ */
222
+ int: (): FieldBuilder => new FieldBuilder("int"),
223
+
224
+ /**
225
+ * Decimal field (REAL/DECIMAL)
226
+ */
227
+ decimal: (): FieldBuilder => new FieldBuilder("decimal"),
228
+
229
+ /**
230
+ * Boolean field (INTEGER 0/1 in SQLite)
231
+ */
232
+ bool: (): FieldBuilder => new FieldBuilder("bool"),
233
+
234
+ /**
235
+ * Timestamp field (TEXT in ISO format)
236
+ */
237
+ timestamp: (): FieldBuilder => new FieldBuilder("timestamp"),
238
+
239
+ /**
240
+ * JSON field (TEXT with JSON serialization)
241
+ */
242
+ json: (): FieldBuilder => new FieldBuilder("json"),
243
+ };
244
+
@@ -0,0 +1,171 @@
1
+ /**
2
+ * String Helper Functions
3
+ *
4
+ * Utilities for transforming table/field names.
5
+ */
6
+
7
+ /**
8
+ * Convert plural word to singular
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * singularize('products') // 'product'
13
+ * singularize('categories') // 'category'
14
+ * singularize('users') // 'user'
15
+ * ```
16
+ */
17
+ export function singularize(word: string): string {
18
+ const irregulars: Record<string, string> = {
19
+ people: "person",
20
+ children: "child",
21
+ men: "man",
22
+ women: "woman",
23
+ teeth: "tooth",
24
+ feet: "foot",
25
+ mice: "mouse",
26
+ geese: "goose",
27
+ };
28
+
29
+ if (irregulars[word]) {
30
+ return irregulars[word];
31
+ }
32
+
33
+ // -ies → -y (categories → category)
34
+ if (word.endsWith("ies")) {
35
+ return word.slice(0, -3) + "y";
36
+ }
37
+
38
+ // -ses, -xes, -zes, -ches, -shes → remove 'es'
39
+ if (
40
+ word.endsWith("ses") ||
41
+ word.endsWith("xes") ||
42
+ word.endsWith("zes") ||
43
+ word.endsWith("ches") ||
44
+ word.endsWith("shes")
45
+ ) {
46
+ return word.slice(0, -2);
47
+ }
48
+
49
+ // -es → remove 'es' for words ending in -o
50
+ if (word.endsWith("oes")) {
51
+ return word.slice(0, -2);
52
+ }
53
+
54
+ // -s → remove 's'
55
+ if (word.endsWith("s") && !word.endsWith("ss")) {
56
+ return word.slice(0, -1);
57
+ }
58
+
59
+ return word;
60
+ }
61
+
62
+ /**
63
+ * Convert string to PascalCase
64
+ */
65
+ export function toPascalCase(str: string): string {
66
+ return str
67
+ .split(/[_\-\s]+/)
68
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
69
+ .join("");
70
+ }
71
+
72
+ /**
73
+ * Convert string to camelCase
74
+ */
75
+ export function toCamelCase(str: string): string {
76
+ const pascal = toPascalCase(str);
77
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
78
+ }
79
+
80
+ /**
81
+ * Convert string to snake_case
82
+ */
83
+ export function toSnakeCase(str: string): string {
84
+ return str
85
+ .replace(/([A-Z])/g, "_$1")
86
+ .replace(/[-\s]+/g, "_")
87
+ .toLowerCase()
88
+ .replace(/^_/, "");
89
+ }
90
+
91
+ /**
92
+ * Get singular PascalCase name for interface
93
+ */
94
+ export function toInterfaceName(tableName: string): string {
95
+ return toPascalCase(singularize(tableName));
96
+ }
97
+
98
+ /**
99
+ * Get Db-prefixed interface name
100
+ * @example toDbInterfaceName('products') // 'DbProduct'
101
+ */
102
+ export function toDbInterfaceName(tableName: string): string {
103
+ return "Db" + toInterfaceName(tableName);
104
+ }
105
+
106
+ /**
107
+ * Pluralize a word (basic implementation)
108
+ */
109
+ export function pluralize(word: string): string {
110
+ const irregulars: Record<string, string> = {
111
+ person: "people",
112
+ child: "children",
113
+ man: "men",
114
+ woman: "women",
115
+ tooth: "teeth",
116
+ foot: "feet",
117
+ mouse: "mice",
118
+ goose: "geese",
119
+ };
120
+
121
+ if (irregulars[word]) {
122
+ return irregulars[word];
123
+ }
124
+
125
+ // -y → -ies (category → categories)
126
+ if (word.endsWith("y") && !/[aeiou]y$/.test(word)) {
127
+ return word.slice(0, -1) + "ies";
128
+ }
129
+
130
+ // -s, -x, -z, -ch, -sh → add 'es'
131
+ if (
132
+ word.endsWith("s") ||
133
+ word.endsWith("x") ||
134
+ word.endsWith("z") ||
135
+ word.endsWith("ch") ||
136
+ word.endsWith("sh")
137
+ ) {
138
+ return word + "es";
139
+ }
140
+
141
+ // -o → add 'es' for common words
142
+ if (word.endsWith("o") && /[^aeiou]o$/.test(word)) {
143
+ return word + "es";
144
+ }
145
+
146
+ // Default: add 's'
147
+ return word + "s";
148
+ }
149
+
150
+ /**
151
+ * Get PascalCase plural form
152
+ * @example toPascalCasePlural('products') // 'Products'
153
+ */
154
+ export function toPascalCasePlural(tableName: string): string {
155
+ // Table names are usually already plural, just convert to PascalCase
156
+ return toPascalCase(tableName);
157
+ }
158
+
159
+ /**
160
+ * Get translation table name
161
+ */
162
+ export function toTranslationTableName(tableName: string): string {
163
+ return `${tableName}_translations`;
164
+ }
165
+
166
+ /**
167
+ * Get foreign key column name for translation table
168
+ */
169
+ export function toTranslationFKName(tableName: string): string {
170
+ return `${singularize(tableName)}_id`;
171
+ }