@promakeai/orm 1.0.6 → 1.3.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.
@@ -4,9 +4,12 @@
4
4
  * Validates schema definitions for correctness.
5
5
  */
6
6
 
7
- import type { SchemaDefinition, TableDefinition } from "../types";
7
+ import type { SchemaDefinition, TableDefinition, PermissionRole, PermissionAction } from "../types";
8
8
  import { getRefTarget } from "./schemaHelpers";
9
9
 
10
+ const VALID_ROLES: PermissionRole[] = ["anon", "user", "admin"];
11
+ const VALID_ACTIONS: PermissionAction[] = ["create", "read", "update", "delete"];
12
+
10
13
  /**
11
14
  * Validation error with context
12
15
  */
@@ -28,6 +31,8 @@ export const ValidationErrorCode = {
28
31
  DUPLICATE_TABLE: "DUPLICATE_TABLE",
29
32
  RESERVED_FIELD_NAME: "RESERVED_FIELD_NAME",
30
33
  SELF_REFERENCE_ON_REQUIRED: "SELF_REFERENCE_ON_REQUIRED",
34
+ INVALID_PERMISSION_ROLE: "INVALID_PERMISSION_ROLE",
35
+ INVALID_PERMISSION_ACTION: "INVALID_PERMISSION_ACTION",
31
36
  } as const;
32
37
 
33
38
  /**
@@ -113,6 +118,33 @@ export function validateSchema(schema: SchemaDefinition): ValidationError[] {
113
118
  });
114
119
  }
115
120
  }
121
+
122
+ // Validate $permissions if present
123
+ if (table.permissions) {
124
+ for (const [role, actions] of Object.entries(table.permissions)) {
125
+ if (!VALID_ROLES.includes(role as PermissionRole)) {
126
+ errors.push({
127
+ table: tableName,
128
+ field: "$permissions",
129
+ message: `Invalid permission role: "${role}". Valid roles: ${VALID_ROLES.join(", ")}`,
130
+ code: ValidationErrorCode.INVALID_PERMISSION_ROLE,
131
+ });
132
+ }
133
+
134
+ if (Array.isArray(actions)) {
135
+ for (const action of actions) {
136
+ if (!VALID_ACTIONS.includes(action as PermissionAction)) {
137
+ errors.push({
138
+ table: tableName,
139
+ field: "$permissions",
140
+ message: `Invalid permission action: "${action}". Valid actions: ${VALID_ACTIONS.join(", ")}`,
141
+ code: ValidationErrorCode.INVALID_PERMISSION_ACTION,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
116
148
  }
117
149
 
118
150
  return errors;
package/src/types.ts CHANGED
@@ -46,7 +46,7 @@ export interface FieldReference {
46
46
  }
47
47
 
48
48
  /**
49
- * Complete field definition after DSL processing
49
+ * Complete field definition (normalized from JSON schema)
50
50
  */
51
51
  export interface FieldDefinition {
52
52
  type: FieldType;
@@ -76,6 +76,25 @@ export interface FieldDefinition {
76
76
  match?: string; // RegExp pattern as string
77
77
  }
78
78
 
79
+ // ==================== Permission Types ====================
80
+
81
+ /**
82
+ * Permission roles for table access control
83
+ */
84
+ export type PermissionRole = "anon" | "user" | "admin";
85
+
86
+ /**
87
+ * Permission actions
88
+ */
89
+ export type PermissionAction = "create" | "read" | "update" | "delete";
90
+
91
+ /**
92
+ * Table-level permissions mapping roles to allowed actions.
93
+ * If not defined on a table, no restrictions apply (backward compatible).
94
+ * Enforced by the backend, not by the ORM.
95
+ */
96
+ export type TablePermissions = Partial<Record<PermissionRole, PermissionAction[]>>;
97
+
79
98
  // ==================== Table & Schema Types ====================
80
99
 
81
100
  /**
@@ -84,6 +103,7 @@ export interface FieldDefinition {
84
103
  export interface TableDefinition {
85
104
  name: string;
86
105
  fields: Record<string, FieldDefinition>;
106
+ permissions?: TablePermissions;
87
107
  }
88
108
 
89
109
  /**
@@ -103,22 +123,6 @@ export interface SchemaDefinition {
103
123
  tables: Record<string, TableDefinition>;
104
124
  }
105
125
 
106
- /**
107
- * Interface for FieldBuilder-like objects (for type checking)
108
- */
109
- export interface FieldBuilderLike {
110
- build(): FieldDefinition;
111
- }
112
-
113
- /**
114
- * Raw schema input before processing (from user DSL)
115
- */
116
- export interface SchemaInput {
117
- name?: string;
118
- languages: string[] | LanguageConfig;
119
- tables: Record<string, Record<string, FieldBuilderLike>>;
120
- }
121
-
122
126
  // ==================== JSON Schema Types (AI Agent Friendly) ====================
123
127
 
124
128
  /**
@@ -5,7 +5,7 @@
5
5
  * Enables AI agents to work with JSON while maintaining type-safe internal representation.
6
6
  */
7
7
 
8
- import type {
8
+ import type {
9
9
  JSONSchemaDefinition,
10
10
  JSONFieldDefinition,
11
11
  JSONFieldType,
@@ -13,23 +13,24 @@ import type {
13
13
  FieldDefinition,
14
14
  FieldType,
15
15
  TableDefinition,
16
- LanguageConfig,
17
- } from "../types";
18
-
19
- const LANGUAGE_NAME_TO_CODE: Record<string, string> = {
20
- english: "en",
21
- turkish: "tr",
22
- german: "de",
23
- french: "fr",
24
- spanish: "es",
25
- russian: "ru",
26
- arabic: "ar",
27
- };
28
-
29
- export interface ParseJSONSchemaResult {
30
- schema: SchemaDefinition;
31
- warnings: string[];
32
- }
16
+ LanguageConfig,
17
+ TablePermissions,
18
+ } from "../types";
19
+
20
+ const LANGUAGE_NAME_TO_CODE: Record<string, string> = {
21
+ english: "en",
22
+ turkish: "tr",
23
+ german: "de",
24
+ french: "fr",
25
+ spanish: "es",
26
+ russian: "ru",
27
+ arabic: "ar",
28
+ };
29
+
30
+ export interface ParseJSONSchemaResult {
31
+ schema: SchemaDefinition;
32
+ warnings: string[];
33
+ }
33
34
 
34
35
  /**
35
36
  * Convert JSON schema to internal SchemaDefinition
@@ -52,119 +53,125 @@ export interface ParseJSONSchemaResult {
52
53
  * const schema = parseJSONSchema(jsonSchema);
53
54
  * ```
54
55
  */
55
- export function parseJSONSchema(json: JSONSchemaDefinition): SchemaDefinition {
56
- return parseJSONSchemaWithWarnings(json).schema;
57
- }
58
-
59
- export function parseJSONSchemaWithWarnings(
60
- json: JSONSchemaDefinition
61
- ): ParseJSONSchemaResult {
62
- const warnings: string[] = [];
63
- const supported = (json.languages || ["en"]).map((lang) => normalizeLangToken(lang));
64
- const defaultLanguage = resolveDefaultLanguage(
65
- json.defaultLanguage,
66
- supported,
67
- warnings
68
- );
69
-
70
- const languages: LanguageConfig = {
71
- default: defaultLanguage,
72
- supported,
73
- };
56
+ export function parseJSONSchema(json: JSONSchemaDefinition): SchemaDefinition {
57
+ return parseJSONSchemaWithWarnings(json).schema;
58
+ }
59
+
60
+ export function parseJSONSchemaWithWarnings(
61
+ json: JSONSchemaDefinition
62
+ ): ParseJSONSchemaResult {
63
+ const warnings: string[] = [];
64
+ const supported = (json.languages || ["en"]).map((lang) => normalizeLangToken(lang));
65
+ const defaultLanguage = resolveDefaultLanguage(
66
+ json.defaultLanguage,
67
+ supported,
68
+ warnings
69
+ );
70
+
71
+ const languages: LanguageConfig = {
72
+ default: defaultLanguage,
73
+ supported,
74
+ };
74
75
 
75
76
  const tables: Record<string, TableDefinition> = {};
76
77
 
77
78
  for (const [tableName, jsonTable] of Object.entries(json.tables)) {
78
79
  const fields: Record<string, FieldDefinition> = {};
80
+ let permissions: TablePermissions | undefined;
79
81
 
80
82
  for (const [fieldName, jsonField] of Object.entries(jsonTable)) {
81
- fields[fieldName] = convertJSONField(jsonField);
83
+ if (fieldName === "$permissions") {
84
+ permissions = jsonField as unknown as TablePermissions;
85
+ continue;
86
+ }
87
+ fields[fieldName] = convertJSONField(jsonField as JSONFieldDefinition);
82
88
  }
83
89
 
84
90
  tables[tableName] = {
85
91
  name: tableName,
86
92
  fields,
93
+ ...(permissions ? { permissions } : {}),
87
94
  };
88
95
  }
89
96
 
90
- return {
91
- schema: {
92
- name: json.name,
93
- languages,
94
- tables,
95
- },
96
- warnings,
97
- };
98
- }
99
-
100
- function normalizeLangToken(lang: string): string {
101
- return String(lang).trim().toLowerCase();
102
- }
103
-
104
- function resolveDefaultLanguage(
105
- input: string | undefined,
106
- supported: string[],
107
- warnings: string[]
108
- ): string {
109
- const fallback = supported[0] || "en";
110
- if (!input) return fallback;
111
-
112
- const normalizedInput = normalizeLangToken(input);
113
- const directMatch = supported.find(
114
- (lang) => normalizeLangToken(lang) === normalizedInput
115
- );
116
- if (directMatch) return directMatch;
117
-
118
- const languageNameMatch = resolveLanguageNameToSupportedCode(
119
- normalizedInput,
120
- supported
121
- );
122
- if (languageNameMatch) {
123
- warnings.push(
124
- `Normalized defaultLanguage "${input}" to "${languageNameMatch}".`
125
- );
126
- return languageNameMatch;
127
- }
128
-
129
- throw new Error(
130
- `Invalid defaultLanguage "${input}". Expected one of: ${supported.join(", ")}`
131
- );
132
- }
133
-
134
- function resolveLanguageNameToSupportedCode(
135
- normalizedInput: string,
136
- supported: string[]
137
- ): string | null {
138
- const mapped = LANGUAGE_NAME_TO_CODE[normalizedInput];
139
- if (mapped) {
140
- const supportedMatch = supported.find(
141
- (lang) => normalizeLangToken(lang) === mapped
142
- );
143
- if (supportedMatch) return supportedMatch;
144
- }
145
-
146
- for (const code of supported) {
147
- const languageName = getLanguageDisplayName(code);
148
- if (languageName && normalizeLangToken(languageName) === normalizedInput) {
149
- return code;
150
- }
151
- }
152
-
153
- return null;
154
- }
155
-
156
- function getLanguageDisplayName(code: string): string | null {
157
- try {
158
- if (typeof Intl === "undefined" || !("DisplayNames" in Intl)) {
159
- return null;
160
- }
161
- const displayNames = new Intl.DisplayNames(["en"], { type: "language" });
162
- const label = displayNames.of(code);
163
- return label || null;
164
- } catch {
165
- return null;
166
- }
167
- }
97
+ return {
98
+ schema: {
99
+ name: json.name,
100
+ languages,
101
+ tables,
102
+ },
103
+ warnings,
104
+ };
105
+ }
106
+
107
+ function normalizeLangToken(lang: string): string {
108
+ return String(lang).trim().toLowerCase();
109
+ }
110
+
111
+ function resolveDefaultLanguage(
112
+ input: string | undefined,
113
+ supported: string[],
114
+ warnings: string[]
115
+ ): string {
116
+ const fallback = supported[0] || "en";
117
+ if (!input) return fallback;
118
+
119
+ const normalizedInput = normalizeLangToken(input);
120
+ const directMatch = supported.find(
121
+ (lang) => normalizeLangToken(lang) === normalizedInput
122
+ );
123
+ if (directMatch) return directMatch;
124
+
125
+ const languageNameMatch = resolveLanguageNameToSupportedCode(
126
+ normalizedInput,
127
+ supported
128
+ );
129
+ if (languageNameMatch) {
130
+ warnings.push(
131
+ `Normalized defaultLanguage "${input}" to "${languageNameMatch}".`
132
+ );
133
+ return languageNameMatch;
134
+ }
135
+
136
+ throw new Error(
137
+ `Invalid defaultLanguage "${input}". Expected one of: ${supported.join(", ")}`
138
+ );
139
+ }
140
+
141
+ function resolveLanguageNameToSupportedCode(
142
+ normalizedInput: string,
143
+ supported: string[]
144
+ ): string | null {
145
+ const mapped = LANGUAGE_NAME_TO_CODE[normalizedInput];
146
+ if (mapped) {
147
+ const supportedMatch = supported.find(
148
+ (lang) => normalizeLangToken(lang) === mapped
149
+ );
150
+ if (supportedMatch) return supportedMatch;
151
+ }
152
+
153
+ for (const code of supported) {
154
+ const languageName = getLanguageDisplayName(code);
155
+ if (languageName && normalizeLangToken(languageName) === normalizedInput) {
156
+ return code;
157
+ }
158
+ }
159
+
160
+ return null;
161
+ }
162
+
163
+ function getLanguageDisplayName(code: string): string | null {
164
+ try {
165
+ if (typeof Intl === "undefined" || !("DisplayNames" in Intl)) {
166
+ return null;
167
+ }
168
+ const displayNames = new Intl.DisplayNames(["en"], { type: "language" });
169
+ const label = displayNames.of(code);
170
+ return label || null;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
168
175
 
169
176
  /**
170
177
  * Normalize JSON-native type syntax to internal FieldType + properties
@@ -225,7 +232,7 @@ function convertJSONField(jsonField: JSONFieldDefinition): FieldDefinition {
225
232
  type,
226
233
  nullable: jsonField.nullable ?? !jsonField.required,
227
234
  unique: jsonField.unique ?? false,
228
- primary: jsonField.primary ?? false,
235
+ primary: jsonField.primary ?? (type === "id"),
229
236
  default: jsonField.default,
230
237
  translatable: jsonField.translatable ?? false,
231
238
  properties,
@@ -44,21 +44,21 @@ function buildAliasedWhereClause(
44
44
  /**
45
45
  * Options for translation queries
46
46
  */
47
- export interface TranslationQueryOptions {
48
- table: string;
49
- schema: SchemaDefinition;
50
- lang: string;
51
- fallbackLang?: string;
52
- /**
53
- * Main-table fields that can be safely used as cache fallback in COALESCE.
54
- * If omitted, all translatable fields are assumed available in main table.
55
- */
56
- mainFallbackFields?: string[];
57
- where?: Record<string, unknown>;
58
- orderBy?: OrderByOption[];
59
- limit?: number;
60
- offset?: number;
61
- }
47
+ export interface TranslationQueryOptions {
48
+ table: string;
49
+ schema: SchemaDefinition;
50
+ lang: string;
51
+ fallbackLang?: string;
52
+ /**
53
+ * Main-table fields that can be safely used as cache fallback in COALESCE.
54
+ * If omitted, all translatable fields are assumed available in main table.
55
+ */
56
+ mainFallbackFields?: string[];
57
+ where?: Record<string, unknown>;
58
+ orderBy?: OrderByOption[];
59
+ limit?: number;
60
+ offset?: number;
61
+ }
62
62
 
63
63
  /**
64
64
  * Query result with SQL and parameters
@@ -78,13 +78,13 @@ export function buildTranslationQuery(
78
78
  table,
79
79
  schema,
80
80
  lang,
81
- fallbackLang = schema.languages.default,
82
- mainFallbackFields,
83
- where,
84
- orderBy,
85
- limit,
86
- offset,
87
- } = options;
81
+ fallbackLang = schema.languages.default,
82
+ mainFallbackFields,
83
+ where,
84
+ orderBy,
85
+ limit,
86
+ offset,
87
+ } = options;
88
88
 
89
89
  const tableSchema = schema.tables[table];
90
90
  if (!tableSchema) {
@@ -100,32 +100,32 @@ export function buildTranslationQuery(
100
100
  const transTable = toTranslationTableName(table);
101
101
  const fkName = toTranslationFKName(table);
102
102
 
103
- const mainAlias = "m";
104
- const transAlias = "t";
105
- const fallbackAlias = "fb";
106
- const mainFallbackSet =
107
- mainFallbackFields !== undefined
108
- ? new Set(mainFallbackFields)
109
- : null;
103
+ const mainAlias = "m";
104
+ const transAlias = "t";
105
+ const fallbackAlias = "fb";
106
+ const mainFallbackSet =
107
+ mainFallbackFields !== undefined
108
+ ? new Set(mainFallbackFields)
109
+ : null;
110
110
 
111
111
  // Build SELECT fields
112
112
  const selectFields: string[] = [];
113
113
 
114
- for (const [fieldName, field] of Object.entries(tableSchema.fields)) {
115
- if (field.translatable) {
116
- if (mainFallbackSet && !mainFallbackSet.has(fieldName)) {
117
- selectFields.push(
118
- `COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}) as ${fieldName}`
119
- );
120
- } else {
121
- selectFields.push(
122
- `COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}, ${mainAlias}.${fieldName}) as ${fieldName}`
123
- );
124
- }
125
- } else {
126
- selectFields.push(`${mainAlias}.${fieldName}`);
127
- }
128
- }
114
+ for (const [fieldName, field] of Object.entries(tableSchema.fields)) {
115
+ if (field.translatable) {
116
+ if (mainFallbackSet && !mainFallbackSet.has(fieldName)) {
117
+ selectFields.push(
118
+ `COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}) as ${fieldName}`
119
+ );
120
+ } else {
121
+ selectFields.push(
122
+ `COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}, ${mainAlias}.${fieldName}) as ${fieldName}`
123
+ );
124
+ }
125
+ } else {
126
+ selectFields.push(`${mainAlias}.${fieldName}`);
127
+ }
128
+ }
129
129
 
130
130
  let sql = `SELECT ${selectFields.join(", ")}
131
131
  FROM ${table} ${mainAlias}
@@ -170,24 +170,24 @@ LEFT JOIN ${transTable} ${fallbackAlias}
170
170
  /**
171
171
  * Build query for single record by ID with translations
172
172
  */
173
- export function buildTranslationQueryById(
174
- table: string,
175
- schema: SchemaDefinition,
176
- id: number | string,
177
- lang: string,
178
- fallbackLang?: string,
179
- mainFallbackFields?: string[]
180
- ): TranslationQueryResult {
181
- return buildTranslationQuery({
182
- table,
183
- schema,
184
- lang,
185
- fallbackLang,
186
- mainFallbackFields,
187
- where: { id },
188
- limit: 1,
189
- });
190
- }
173
+ export function buildTranslationQueryById(
174
+ table: string,
175
+ schema: SchemaDefinition,
176
+ id: number | string,
177
+ lang: string,
178
+ fallbackLang?: string,
179
+ mainFallbackFields?: string[]
180
+ ): TranslationQueryResult {
181
+ return buildTranslationQuery({
182
+ table,
183
+ schema,
184
+ lang,
185
+ fallbackLang,
186
+ mainFallbackFields,
187
+ where: { id },
188
+ limit: 1,
189
+ });
190
+ }
191
191
 
192
192
  /**
193
193
  * Build simple query without translations