@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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Schema Module Exports
3
+ */
4
+
5
+ // Main API
6
+ export { defineSchema, f, mergeSchemas, createSchemaUnsafe } from "./defineSchema";
7
+ export type { FieldBuilder } from "./fieldBuilder";
8
+
9
+ // Validation
10
+ export {
11
+ validateSchema,
12
+ assertValidSchema,
13
+ isValidSchema,
14
+ validateTable,
15
+ ValidationErrorCode,
16
+ } from "./validator";
17
+ export type { ValidationError } from "./validator";
18
+
19
+ // String Helpers
20
+ export {
21
+ singularize,
22
+ pluralize,
23
+ toPascalCase,
24
+ toCamelCase,
25
+ toSnakeCase,
26
+ toInterfaceName,
27
+ toDbInterfaceName,
28
+ toPascalCasePlural,
29
+ toTranslationTableName,
30
+ toTranslationFKName,
31
+ } from "./helpers";
32
+
33
+ // Schema Helpers
34
+ export {
35
+ getTranslatableFields,
36
+ getNonTranslatableFields,
37
+ getInsertableFields,
38
+ hasTranslatableFields,
39
+ getPrimaryKeyField,
40
+ getReferenceFields,
41
+ getMainTableFields,
42
+ getTranslationTableFields,
43
+ isRequiredField,
44
+ getRequiredFields,
45
+ getRefTarget,
46
+ getRefTargetFull,
47
+ } from "./schemaHelpers";
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Schema Helper Functions
3
+ *
4
+ * Utilities for working with schema definitions.
5
+ */
6
+
7
+ import type { TableDefinition, FieldDefinition, FieldReference } from "../types";
8
+
9
+ /**
10
+ * Get names of translatable fields in a table
11
+ */
12
+ export function getTranslatableFields(table: TableDefinition): string[] {
13
+ return Object.entries(table.fields)
14
+ .filter(([_, field]) => field.translatable)
15
+ .map(([name]) => name);
16
+ }
17
+
18
+ /**
19
+ * Get names of non-translatable fields in a table (excluding primary key)
20
+ */
21
+ export function getNonTranslatableFields(table: TableDefinition): string[] {
22
+ return Object.entries(table.fields)
23
+ .filter(([_, field]) => !field.translatable && !field.primary)
24
+ .map(([name]) => name);
25
+ }
26
+
27
+ /**
28
+ * Get all non-primary key fields (for INSERT statements)
29
+ */
30
+ export function getInsertableFields(table: TableDefinition): string[] {
31
+ return Object.entries(table.fields)
32
+ .filter(([_, field]) => !field.primary)
33
+ .map(([name]) => name);
34
+ }
35
+
36
+ /**
37
+ * Check if table has any translatable fields
38
+ */
39
+ export function hasTranslatableFields(table: TableDefinition): boolean {
40
+ return getTranslatableFields(table).length > 0;
41
+ }
42
+
43
+ /**
44
+ * Get the primary key field name
45
+ */
46
+ export function getPrimaryKeyField(table: TableDefinition): string | null {
47
+ const entry = Object.entries(table.fields).find(([_, field]) => field.primary);
48
+ return entry ? entry[0] : null;
49
+ }
50
+
51
+ /**
52
+ * Get fields with foreign key references (using ref)
53
+ */
54
+ export function getReferenceFields(
55
+ table: TableDefinition
56
+ ): [string, FieldReference][] {
57
+ return Object.entries(table.fields)
58
+ .filter(([_, field]) => field.ref)
59
+ .map(([name, field]) => {
60
+ const ref = field.ref!;
61
+ // Normalize string to FieldReference
62
+ if (typeof ref === "string") {
63
+ return [name, { table: ref, field: "id" }];
64
+ }
65
+ return [name, { ...ref, field: ref.field || "id" }];
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Get fields for main table (excluding translatable)
71
+ */
72
+ export function getMainTableFields(
73
+ table: TableDefinition
74
+ ): [string, FieldDefinition][] {
75
+ return Object.entries(table.fields).filter(([_, field]) => !field.translatable);
76
+ }
77
+
78
+ /**
79
+ * Get fields for translation table
80
+ */
81
+ export function getTranslationTableFields(
82
+ table: TableDefinition
83
+ ): [string, FieldDefinition][] {
84
+ return Object.entries(table.fields).filter(([_, field]) => field.translatable);
85
+ }
86
+
87
+ /**
88
+ * Check if a field is required (NOT NULL without default)
89
+ */
90
+ export function isRequiredField(field: FieldDefinition): boolean {
91
+ return !field.nullable && field.default === undefined && !field.primary;
92
+ }
93
+
94
+ /**
95
+ * Get required fields for input validation
96
+ */
97
+ export function getRequiredFields(table: TableDefinition): string[] {
98
+ return Object.entries(table.fields)
99
+ .filter(([_, field]) => isRequiredField(field))
100
+ .map(([name]) => name);
101
+ }
102
+
103
+ /**
104
+ * Get reference target table name from field
105
+ */
106
+ export function getRefTarget(field: FieldDefinition): string | null {
107
+ if (!field.ref) return null;
108
+ return typeof field.ref === "string" ? field.ref : field.ref.table;
109
+ }
110
+
111
+ /**
112
+ * Get full reference target (table and field) from field
113
+ * Returns { table, field } object
114
+ */
115
+ export function getRefTargetFull(
116
+ field: FieldDefinition
117
+ ): FieldReference | null {
118
+ if (!field.ref) return null;
119
+ if (typeof field.ref === "string") {
120
+ return { table: field.ref, field: "id" };
121
+ }
122
+ return { table: field.ref.table, field: field.ref.field || "id" };
123
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Schema Validator
3
+ *
4
+ * Validates schema definitions for correctness.
5
+ */
6
+
7
+ import type { SchemaDefinition, TableDefinition } from "../types";
8
+ import { getRefTarget } from "./schemaHelpers";
9
+
10
+ /**
11
+ * Validation error with context
12
+ */
13
+ export interface ValidationError {
14
+ table: string;
15
+ field?: string;
16
+ message: string;
17
+ code: string;
18
+ }
19
+
20
+ /**
21
+ * Error codes for validation
22
+ */
23
+ export const ValidationErrorCode = {
24
+ MISSING_PRIMARY_KEY: "MISSING_PRIMARY_KEY",
25
+ INVALID_REFERENCE: "INVALID_REFERENCE",
26
+ INVALID_TRANSLATABLE_TYPE: "INVALID_TRANSLATABLE_TYPE",
27
+ NO_LANGUAGES: "NO_LANGUAGES",
28
+ DUPLICATE_TABLE: "DUPLICATE_TABLE",
29
+ RESERVED_FIELD_NAME: "RESERVED_FIELD_NAME",
30
+ SELF_REFERENCE_ON_REQUIRED: "SELF_REFERENCE_ON_REQUIRED",
31
+ } as const;
32
+
33
+ /**
34
+ * Reserved field names that cannot be used
35
+ */
36
+ const RESERVED_FIELDS = ["language_code"];
37
+
38
+ /**
39
+ * Validate a schema definition
40
+ *
41
+ * @returns Array of validation errors (empty if valid)
42
+ */
43
+ export function validateSchema(schema: SchemaDefinition): ValidationError[] {
44
+ const errors: ValidationError[] = [];
45
+ const tableNames = Object.keys(schema.tables);
46
+
47
+ // Check languages
48
+ if (!schema.languages.supported || schema.languages.supported.length === 0) {
49
+ errors.push({
50
+ table: "_schema",
51
+ message: "At least one language must be defined",
52
+ code: ValidationErrorCode.NO_LANGUAGES,
53
+ });
54
+ }
55
+
56
+ // Validate each table
57
+ for (const [tableName, table] of Object.entries(schema.tables)) {
58
+ // Check for primary key
59
+ const hasPrimaryKey = Object.values(table.fields).some(
60
+ (field) => field.primary
61
+ );
62
+ if (!hasPrimaryKey) {
63
+ errors.push({
64
+ table: tableName,
65
+ message: "Table must have a primary key (use f.id())",
66
+ code: ValidationErrorCode.MISSING_PRIMARY_KEY,
67
+ });
68
+ }
69
+
70
+ // Validate each field
71
+ for (const [fieldName, field] of Object.entries(table.fields)) {
72
+ // Check reserved field names
73
+ if (RESERVED_FIELDS.includes(fieldName)) {
74
+ errors.push({
75
+ table: tableName,
76
+ field: fieldName,
77
+ message: `'${fieldName}' is a reserved field name`,
78
+ code: ValidationErrorCode.RESERVED_FIELD_NAME,
79
+ });
80
+ }
81
+
82
+ // Check foreign key references (using ref)
83
+ const refTarget = getRefTarget(field);
84
+ if (refTarget) {
85
+ // Check if referenced table exists
86
+ if (!tableNames.includes(refTarget)) {
87
+ errors.push({
88
+ table: tableName,
89
+ field: fieldName,
90
+ message: `Foreign key references non-existent table: ${refTarget}`,
91
+ code: ValidationErrorCode.INVALID_REFERENCE,
92
+ });
93
+ }
94
+
95
+ // Check for required self-reference
96
+ if (refTarget === tableName && !field.nullable) {
97
+ errors.push({
98
+ table: tableName,
99
+ field: fieldName,
100
+ message: `Self-referencing foreign key must be nullable`,
101
+ code: ValidationErrorCode.SELF_REFERENCE_ON_REQUIRED,
102
+ });
103
+ }
104
+ }
105
+
106
+ // Check translatable field type
107
+ if (field.translatable && !["string", "text"].includes(field.type)) {
108
+ errors.push({
109
+ table: tableName,
110
+ field: fieldName,
111
+ message: `Only string/text fields can be translatable, got: ${field.type}`,
112
+ code: ValidationErrorCode.INVALID_TRANSLATABLE_TYPE,
113
+ });
114
+ }
115
+ }
116
+ }
117
+
118
+ return errors;
119
+ }
120
+
121
+ /**
122
+ * Validate schema and throw if invalid
123
+ */
124
+ export function assertValidSchema(schema: SchemaDefinition): void {
125
+ const errors = validateSchema(schema);
126
+
127
+ if (errors.length > 0) {
128
+ const messages = errors.map((e) =>
129
+ e.field
130
+ ? `[${e.table}.${e.field}] ${e.message}`
131
+ : `[${e.table}] ${e.message}`
132
+ );
133
+ throw new Error(`Schema validation failed:\n${messages.join("\n")}`);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check if schema is valid (returns boolean)
139
+ */
140
+ export function isValidSchema(schema: SchemaDefinition): boolean {
141
+ return validateSchema(schema).length === 0;
142
+ }
143
+
144
+ /**
145
+ * Validate a single table definition
146
+ */
147
+ export function validateTable(
148
+ tableName: string,
149
+ table: TableDefinition,
150
+ allTableNames: string[]
151
+ ): ValidationError[] {
152
+ const errors: ValidationError[] = [];
153
+
154
+ // Check for primary key
155
+ const hasPrimaryKey = Object.values(table.fields).some(
156
+ (field) => field.primary
157
+ );
158
+ if (!hasPrimaryKey) {
159
+ errors.push({
160
+ table: tableName,
161
+ message: "Table must have a primary key",
162
+ code: ValidationErrorCode.MISSING_PRIMARY_KEY,
163
+ });
164
+ }
165
+
166
+ // Validate fields
167
+ for (const [fieldName, field] of Object.entries(table.fields)) {
168
+ const refTarget = getRefTarget(field);
169
+ if (refTarget && !allTableNames.includes(refTarget)) {
170
+ errors.push({
171
+ table: tableName,
172
+ field: fieldName,
173
+ message: `Foreign key references non-existent table: ${refTarget}`,
174
+ code: ValidationErrorCode.INVALID_REFERENCE,
175
+ });
176
+ }
177
+
178
+ if (field.translatable && !["string", "text"].includes(field.type)) {
179
+ errors.push({
180
+ table: tableName,
181
+ field: fieldName,
182
+ message: `Only string/text fields can be translatable`,
183
+ code: ValidationErrorCode.INVALID_TRANSLATABLE_TYPE,
184
+ });
185
+ }
186
+ }
187
+
188
+ return errors;
189
+ }
package/src/types.ts ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @promakeai/orm Core Type Definitions
3
+ *
4
+ * Database-agnostic types that work in browser and Node.js
5
+ */
6
+
7
+ // ==================== Field Types ====================
8
+
9
+ /**
10
+ * Supported field types
11
+ */
12
+ export type FieldType =
13
+ | "id"
14
+ | "string"
15
+ | "text"
16
+ | "int"
17
+ | "decimal"
18
+ | "bool"
19
+ | "timestamp"
20
+ | "json";
21
+
22
+ /**
23
+ * Foreign key reference definition (MongoDB-style)
24
+ */
25
+ export interface FieldReference {
26
+ table: string;
27
+ field?: string; // Default: 'id'
28
+ onDelete?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
29
+ onUpdate?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
30
+ }
31
+
32
+ /**
33
+ * Complete field definition after DSL processing
34
+ */
35
+ export interface FieldDefinition {
36
+ type: FieldType;
37
+ nullable: boolean;
38
+ unique: boolean;
39
+ primary: boolean;
40
+ default?: unknown;
41
+ translatable: boolean;
42
+
43
+ // Reference (MongoDB-style)
44
+ ref?: string | FieldReference;
45
+
46
+ // Mongoose-style validations & transformations
47
+ required?: boolean;
48
+ trim?: boolean;
49
+ lowercase?: boolean;
50
+ uppercase?: boolean;
51
+ minLength?: number;
52
+ maxLength?: number;
53
+ min?: number;
54
+ max?: number;
55
+ enum?: string[];
56
+ match?: string; // RegExp pattern as string
57
+ }
58
+
59
+ // ==================== Table & Schema Types ====================
60
+
61
+ /**
62
+ * Table definition with all fields
63
+ */
64
+ export interface TableDefinition {
65
+ name: string;
66
+ fields: Record<string, FieldDefinition>;
67
+ }
68
+
69
+ /**
70
+ * Language configuration
71
+ */
72
+ export interface LanguageConfig {
73
+ default: string;
74
+ supported: string[];
75
+ }
76
+
77
+ /**
78
+ * Complete schema definition
79
+ */
80
+ export interface SchemaDefinition {
81
+ name?: string;
82
+ languages: LanguageConfig;
83
+ tables: Record<string, TableDefinition>;
84
+ }
85
+
86
+ /**
87
+ * Interface for FieldBuilder-like objects (for type checking)
88
+ */
89
+ export interface FieldBuilderLike {
90
+ build(): FieldDefinition;
91
+ }
92
+
93
+ /**
94
+ * Raw schema input before processing (from user DSL)
95
+ */
96
+ export interface SchemaInput {
97
+ name?: string;
98
+ languages: string[] | LanguageConfig;
99
+ tables: Record<string, Record<string, FieldBuilderLike>>;
100
+ }
101
+
102
+ // ==================== JSON Schema Types (AI Agent Friendly) ====================
103
+
104
+ /**
105
+ * JSON Schema Field Definition
106
+ */
107
+ export interface JSONFieldDefinition {
108
+ type: FieldType;
109
+ translatable?: boolean;
110
+ required?: boolean;
111
+ unique?: boolean;
112
+ primary?: boolean;
113
+ nullable?: boolean;
114
+ default?: unknown;
115
+
116
+ // Numeric fields
117
+ precision?: number;
118
+ scale?: number;
119
+ min?: number;
120
+ max?: number;
121
+
122
+ // String fields
123
+ trim?: boolean;
124
+ lowercase?: boolean;
125
+ uppercase?: boolean;
126
+ minLength?: number;
127
+ maxLength?: number;
128
+ enum?: string[];
129
+ match?: string;
130
+
131
+ // Reference (MongoDB-style)
132
+ ref?:
133
+ | string
134
+ | {
135
+ table: string;
136
+ field?: string;
137
+ onDelete?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
138
+ onUpdate?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
139
+ };
140
+ }
141
+
142
+ /**
143
+ * JSON Schema Table Definition
144
+ */
145
+ export interface JSONTableDefinition {
146
+ [fieldName: string]: JSONFieldDefinition;
147
+ }
148
+
149
+ /**
150
+ * Complete JSON Schema Definition
151
+ */
152
+ export interface JSONSchemaDefinition {
153
+ name?: string;
154
+ languages: string[];
155
+ defaultLanguage?: string;
156
+ tables: {
157
+ [tableName: string]: JSONTableDefinition;
158
+ };
159
+ }
160
+
161
+ // ==================== Query Types ====================
162
+
163
+ /**
164
+ * MongoDB-style WHERE operators
165
+ */
166
+ export interface WhereCondition {
167
+ $eq?: unknown;
168
+ $ne?: unknown;
169
+ $gt?: number;
170
+ $gte?: number;
171
+ $lt?: number;
172
+ $lte?: number;
173
+ $in?: unknown[];
174
+ $nin?: unknown[];
175
+ $like?: string;
176
+ $notLike?: string;
177
+ $between?: [unknown, unknown];
178
+ $isNull?: boolean;
179
+ $not?: WhereCondition;
180
+ $and?: WhereCondition[];
181
+ $or?: WhereCondition[];
182
+ $nor?: WhereCondition[];
183
+ [key: string]: unknown;
184
+ }
185
+
186
+ /**
187
+ * Order by option
188
+ */
189
+ export interface OrderByOption {
190
+ field: string;
191
+ direction: "ASC" | "DESC";
192
+ }
193
+
194
+ /**
195
+ * Populate option for resolving references (MongoDB-style)
196
+ */
197
+ export interface PopulateNested {
198
+ populate?: PopulateOption;
199
+ where?: Record<string, unknown>;
200
+ limit?: number;
201
+ orderBy?: OrderByOption[];
202
+ }
203
+
204
+ export type PopulateOption =
205
+ | string // 'userId categoryIds'
206
+ | string[] // ['userId', 'categoryIds']
207
+ | Record<string, boolean | PopulateNested>; // { userId: true, categoryIds: {...} }
208
+
209
+ /**
210
+ * Query options
211
+ */
212
+ export interface QueryOptions {
213
+ where?: Record<string, unknown>;
214
+ populate?: PopulateOption;
215
+ orderBy?: OrderByOption[];
216
+ limit?: number;
217
+ offset?: number;
218
+ lang?: string;
219
+ fallbackLang?: string;
220
+ }
221
+
222
+ /**
223
+ * Paginated result
224
+ */
225
+ export interface PaginatedResult<T> {
226
+ data: T[];
227
+ page: number;
228
+ limit: number;
229
+ total: number;
230
+ totalPages: number;
231
+ hasMore: boolean;
232
+ }
233
+
234
+ // ==================== ORM Config ====================
235
+
236
+ /**
237
+ * ORM Configuration
238
+ */
239
+ export interface ORMConfig {
240
+ schema?: SchemaDefinition;
241
+ defaultLang?: string;
242
+ fallbackLang?: string;
243
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * JSON Schema Converter
3
+ *
4
+ * Bidirectional conversion between JSON schema format and internal SchemaDefinition.
5
+ * Enables AI agents to work with JSON while maintaining type-safe internal representation.
6
+ */
7
+
8
+ import type {
9
+ JSONSchemaDefinition,
10
+ JSONFieldDefinition,
11
+ SchemaDefinition,
12
+ FieldDefinition,
13
+ TableDefinition,
14
+ LanguageConfig,
15
+ } from "../types";
16
+
17
+ /**
18
+ * Convert JSON schema to internal SchemaDefinition
19
+ *
20
+ * @param json - JSON schema from AI agent or file
21
+ * @returns Internal SchemaDefinition for processing
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const jsonSchema = {
26
+ * name: "products",
27
+ * languages: ["en", "tr"],
28
+ * tables: {
29
+ * products: {
30
+ * id: { type: "id", primary: true },
31
+ * name: { type: "string", translatable: true, required: true }
32
+ * }
33
+ * }
34
+ * };
35
+ * const schema = parseJSONSchema(jsonSchema);
36
+ * ```
37
+ */
38
+ export function parseJSONSchema(json: JSONSchemaDefinition): SchemaDefinition {
39
+ const languages: LanguageConfig = {
40
+ default: json.defaultLanguage || json.languages[0] || "en",
41
+ supported: json.languages || ["en"],
42
+ };
43
+
44
+ const tables: Record<string, TableDefinition> = {};
45
+
46
+ for (const [tableName, jsonTable] of Object.entries(json.tables)) {
47
+ const fields: Record<string, FieldDefinition> = {};
48
+
49
+ for (const [fieldName, jsonField] of Object.entries(jsonTable)) {
50
+ fields[fieldName] = convertJSONField(jsonField);
51
+ }
52
+
53
+ tables[tableName] = {
54
+ name: tableName,
55
+ fields,
56
+ };
57
+ }
58
+
59
+ return {
60
+ name: json.name,
61
+ languages,
62
+ tables,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Convert single JSON field to internal FieldDefinition
68
+ */
69
+ function convertJSONField(jsonField: JSONFieldDefinition): FieldDefinition {
70
+ // Convert ref to FieldReference format
71
+ let ref: FieldDefinition["ref"];
72
+ if (jsonField.ref) {
73
+ if (typeof jsonField.ref === "string") {
74
+ ref = jsonField.ref;
75
+ } else {
76
+ ref = {
77
+ table: jsonField.ref.table,
78
+ field: jsonField.ref.field,
79
+ onDelete: jsonField.ref.onDelete,
80
+ onUpdate: jsonField.ref.onUpdate,
81
+ };
82
+ }
83
+ }
84
+
85
+ return {
86
+ type: jsonField.type,
87
+ nullable: jsonField.nullable ?? !jsonField.required,
88
+ unique: jsonField.unique ?? false,
89
+ primary: jsonField.primary ?? false,
90
+ default: jsonField.default,
91
+ translatable: jsonField.translatable ?? false,
92
+ ref,
93
+ };
94
+ }