@famgia/omnify-core 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @famgia/omnify-core
2
2
 
3
- Core engine for the Omnify schema system. Load, validate, and process YAML/JSON schemas.
3
+ Core engine for the Omnify schema system. Provides schema loading, validation, plugin management, and generator execution.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,40 +10,247 @@ npm install @famgia/omnify-core
10
10
 
11
11
  ## Usage
12
12
 
13
+ ### Load and Validate Schemas
14
+
13
15
  ```typescript
14
- import {
15
- loadSchemas,
16
- validateSchemas,
17
- createOmnify,
18
- Omnify
19
- } from '@famgia/omnify-core';
16
+ import { loadSchemas, validateSchemas } from '@famgia/omnify-core';
20
17
 
21
18
  // Load schemas from directory
22
19
  const schemas = await loadSchemas('./schemas');
23
20
 
24
21
  // Validate schemas
25
22
  const result = validateSchemas(schemas);
23
+
26
24
  if (!result.valid) {
27
- console.error(result.errors);
25
+ for (const error of result.errors) {
26
+ console.error(`${error.schemaName}: ${error.message}`);
27
+ }
28
28
  }
29
+ ```
30
+
31
+ ### Plugin Manager
32
+
33
+ ```typescript
34
+ import { PluginManager } from '@famgia/omnify-core';
35
+ import type { OmnifyPlugin } from '@famgia/omnify-types';
36
+
37
+ const manager = new PluginManager({
38
+ cwd: process.cwd(),
39
+ verbose: true,
40
+ logger: console,
41
+ });
42
+
43
+ // Register a plugin
44
+ await manager.register(myPlugin);
45
+
46
+ // Get all registered custom types
47
+ const customTypes = manager.getCustomTypes();
48
+
49
+ // Run all generators
50
+ const result = await manager.runGenerators(schemas);
51
+
52
+ if (result.success) {
53
+ for (const output of result.outputs) {
54
+ console.log(`Generated: ${output.path}`);
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Generator Runner (DAG-based)
60
+
61
+ ```typescript
62
+ import { GeneratorRunner } from '@famgia/omnify-core';
29
63
 
30
- // Or use the Omnify class for full API
31
- const omnify = createOmnify({
32
- schemaDir: './schemas',
33
- plugins: [myPlugin],
64
+ const runner = new GeneratorRunner({
65
+ cwd: process.cwd(),
66
+ verbose: true,
67
+ logger: console,
34
68
  });
35
69
 
36
- await omnify.initialize();
37
- const schemas = omnify.getSchemas();
70
+ // Register generators (automatically sorts by dependencies)
71
+ runner.registerAll([
72
+ {
73
+ definition: schemaGenerator,
74
+ pluginName: 'my-plugin',
75
+ pluginConfig: {},
76
+ },
77
+ {
78
+ definition: migrationGenerator,
79
+ pluginName: 'my-plugin',
80
+ pluginConfig: {},
81
+ },
82
+ ]);
83
+
84
+ // Execute in topological order
85
+ const result = await runner.runAll(schemas);
86
+ ```
87
+
88
+ ### Error Handling
89
+
90
+ ```typescript
91
+ import { OmnifyError } from '@famgia/omnify-core';
92
+
93
+ try {
94
+ await loadSchemas('./invalid-path');
95
+ } catch (error) {
96
+ if (error instanceof OmnifyError) {
97
+ console.error(`[${error.code}] ${error.message}`);
98
+ if (error.schemaName) {
99
+ console.error(` Schema: ${error.schemaName}`);
100
+ }
101
+ if (error.suggestion) {
102
+ console.error(` Suggestion: ${error.suggestion}`);
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## API Reference
109
+
110
+ ### Functions
111
+
112
+ | Function | Description |
113
+ |----------|-------------|
114
+ | `loadSchemas(path)` | Load all YAML/JSON schema files from directory |
115
+ | `loadSchema(filePath)` | Load a single schema file |
116
+ | `validateSchemas(schemas)` | Validate schema collection |
117
+ | `validateSchema(schema)` | Validate a single schema |
118
+
119
+ ### Classes
120
+
121
+ #### `PluginManager`
122
+
123
+ Manages plugin registration and generator execution.
124
+
125
+ ```typescript
126
+ new PluginManager(options: PluginManagerOptions)
127
+
128
+ interface PluginManagerOptions {
129
+ cwd: string;
130
+ verbose?: boolean;
131
+ logger?: PluginLogger;
132
+ }
38
133
  ```
39
134
 
40
- ## Features
135
+ Methods:
136
+ - `register(plugin)` - Register a plugin
137
+ - `getPlugin(name)` - Get plugin by name
138
+ - `getCustomTypes()` - Get all custom types
139
+ - `hasCustomType(name)` - Check if custom type exists
140
+ - `getGenerators()` - Get all registered generators
141
+ - `runGenerators(schemas)` - Execute all generators
142
+
143
+ #### `GeneratorRunner`
144
+
145
+ Executes generators in dependency order using topological sort.
146
+
147
+ ```typescript
148
+ new GeneratorRunner(options: GeneratorRunnerOptions)
149
+ ```
150
+
151
+ Methods:
152
+ - `register(generator)` - Register a single generator
153
+ - `registerAll(generators)` - Register multiple generators
154
+ - `runAll(schemas)` - Execute all generators in order
155
+ - `getOrder()` - Get execution order (for debugging)
156
+
157
+ #### `OmnifyError`
158
+
159
+ Custom error class with detailed error information.
160
+
161
+ ```typescript
162
+ class OmnifyError extends Error {
163
+ code: string;
164
+ schemaName?: string;
165
+ propertyName?: string;
166
+ filePath?: string;
167
+ suggestion?: string;
168
+ }
169
+ ```
170
+
171
+ ## Plugin System
172
+
173
+ ### Creating a Plugin
174
+
175
+ ```typescript
176
+ import type { OmnifyPlugin } from '@famgia/omnify-types';
177
+
178
+ const myPlugin: OmnifyPlugin = {
179
+ name: 'my-plugin',
180
+ version: '1.0.0',
181
+
182
+ // Lifecycle hooks
183
+ setup: async (context) => {
184
+ console.log('Plugin initialized');
185
+ },
186
+
187
+ teardown: async () => {
188
+ console.log('Plugin cleanup');
189
+ },
190
+
191
+ // Custom types
192
+ types: [
193
+ {
194
+ name: 'Money',
195
+ typescript: 'number',
196
+ laravel: 'decimal',
197
+ laravelParams: [10, 2],
198
+ },
199
+ ],
200
+
201
+ // Generators
202
+ generators: [
203
+ {
204
+ name: 'my-generator',
205
+ description: 'Generate custom files',
206
+ dependsOn: ['other-generator'],
207
+
208
+ generate: async (ctx) => {
209
+ return [{
210
+ path: 'output/file.ts',
211
+ content: '// Generated',
212
+ type: 'other',
213
+ }];
214
+ },
215
+ },
216
+ ],
217
+ };
218
+ ```
219
+
220
+ ### Generator Dependencies
221
+
222
+ Generators can specify `dependsOn` to control execution order:
223
+
224
+ ```typescript
225
+ generators: [
226
+ {
227
+ name: 'schema-generator',
228
+ generate: async (ctx) => { /* ... */ },
229
+ },
230
+ {
231
+ name: 'migration-generator',
232
+ dependsOn: ['schema-generator'], // Runs after schema-generator
233
+ generate: async (ctx) => {
234
+ // Access previous outputs
235
+ const schemaOutputs = ctx.previousOutputs.get('schema-generator');
236
+ // ...
237
+ },
238
+ },
239
+ ]
240
+ ```
241
+
242
+ The GeneratorRunner uses Kahn's algorithm for topological sorting and detects circular dependencies.
243
+
244
+ ## Validation Rules
245
+
246
+ The validator checks for:
41
247
 
42
- - YAML/JSON schema loading
43
- - Schema validation with detailed errors
44
- - Plugin system for custom types
45
- - Programmatic API for tooling integration
46
- - Metadata introspection utilities
248
+ - Valid schema structure (name, kind, properties)
249
+ - Valid property types (built-in or custom)
250
+ - Valid association types and references
251
+ - Unique property names
252
+ - Required fields presence
253
+ - Circular reference detection
47
254
 
48
255
  ## Related Packages
49
256
 
package/dist/index.cjs CHANGED
@@ -1156,6 +1156,163 @@ function validateSchemas(schemas, options = {}) {
1156
1156
  };
1157
1157
  }
1158
1158
 
1159
+ // src/plugins/generator-runner.ts
1160
+ var GeneratorRunner = class {
1161
+ generators = /* @__PURE__ */ new Map();
1162
+ options;
1163
+ constructor(options) {
1164
+ this.options = options;
1165
+ }
1166
+ /**
1167
+ * Register a generator.
1168
+ */
1169
+ register(generator) {
1170
+ const name = generator.definition.name;
1171
+ if (this.generators.has(name)) {
1172
+ throw new Error(`Generator "${name}" is already registered`);
1173
+ }
1174
+ this.generators.set(name, generator);
1175
+ }
1176
+ /**
1177
+ * Register multiple generators.
1178
+ */
1179
+ registerAll(generators) {
1180
+ for (const gen of generators) {
1181
+ this.register(gen);
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Clear all registered generators.
1186
+ */
1187
+ clear() {
1188
+ this.generators.clear();
1189
+ }
1190
+ /**
1191
+ * Get all registered generator names.
1192
+ */
1193
+ getGeneratorNames() {
1194
+ return Array.from(this.generators.keys());
1195
+ }
1196
+ /**
1197
+ * Check if a generator is registered.
1198
+ */
1199
+ hasGenerator(name) {
1200
+ return this.generators.has(name);
1201
+ }
1202
+ /**
1203
+ * Run all generators in topological order.
1204
+ */
1205
+ async runAll(schemas) {
1206
+ const errors = [];
1207
+ const outputs = [];
1208
+ const outputsByGenerator = /* @__PURE__ */ new Map();
1209
+ let executionOrder;
1210
+ try {
1211
+ executionOrder = this.topologicalSort();
1212
+ } catch (error) {
1213
+ return {
1214
+ success: false,
1215
+ outputs: [],
1216
+ outputsByGenerator: /* @__PURE__ */ new Map(),
1217
+ executionOrder: [],
1218
+ errors: [{
1219
+ generatorName: "",
1220
+ message: error instanceof Error ? error.message : String(error),
1221
+ cause: error
1222
+ }]
1223
+ };
1224
+ }
1225
+ const previousOutputs = /* @__PURE__ */ new Map();
1226
+ for (const name of executionOrder) {
1227
+ const generator = this.generators.get(name);
1228
+ const { definition, pluginConfig } = generator;
1229
+ try {
1230
+ this.options.logger.debug(`Running generator: ${name}`);
1231
+ const ctx = {
1232
+ schemas,
1233
+ pluginConfig,
1234
+ cwd: this.options.cwd,
1235
+ logger: this.options.logger,
1236
+ previousOutputs
1237
+ };
1238
+ const result = await definition.generate(ctx);
1239
+ const generatorOutputs = Array.isArray(result) ? result : [result];
1240
+ outputsByGenerator.set(name, generatorOutputs);
1241
+ previousOutputs.set(name, generatorOutputs);
1242
+ outputs.push(...generatorOutputs);
1243
+ this.options.logger.debug(`Generator ${name} produced ${generatorOutputs.length} outputs`);
1244
+ } catch (error) {
1245
+ const message = error instanceof Error ? error.message : String(error);
1246
+ this.options.logger.error(`Generator ${name} failed: ${message}`);
1247
+ errors.push({
1248
+ generatorName: name,
1249
+ message,
1250
+ cause: error
1251
+ });
1252
+ }
1253
+ }
1254
+ return {
1255
+ success: errors.length === 0,
1256
+ outputs,
1257
+ outputsByGenerator,
1258
+ executionOrder,
1259
+ errors
1260
+ };
1261
+ }
1262
+ /**
1263
+ * Topological sort using Kahn's algorithm.
1264
+ * Returns generators in execution order (dependencies first).
1265
+ * Throws if circular dependencies detected.
1266
+ */
1267
+ topologicalSort() {
1268
+ const names = Array.from(this.generators.keys());
1269
+ const inDegree = /* @__PURE__ */ new Map();
1270
+ const adjacency = /* @__PURE__ */ new Map();
1271
+ for (const name of names) {
1272
+ inDegree.set(name, 0);
1273
+ adjacency.set(name, []);
1274
+ }
1275
+ for (const name of names) {
1276
+ const generator = this.generators.get(name);
1277
+ const deps = generator.definition.dependsOn ?? [];
1278
+ for (const dep of deps) {
1279
+ if (!this.generators.has(dep)) {
1280
+ throw new Error(
1281
+ `Generator "${name}" depends on "${dep}" which is not registered`
1282
+ );
1283
+ }
1284
+ adjacency.get(dep).push(name);
1285
+ inDegree.set(name, inDegree.get(name) + 1);
1286
+ }
1287
+ }
1288
+ const queue = [];
1289
+ const result = [];
1290
+ for (const [name, degree] of inDegree) {
1291
+ if (degree === 0) {
1292
+ queue.push(name);
1293
+ }
1294
+ }
1295
+ while (queue.length > 0) {
1296
+ const current = queue.shift();
1297
+ result.push(current);
1298
+ for (const neighbor of adjacency.get(current)) {
1299
+ const newDegree = inDegree.get(neighbor) - 1;
1300
+ inDegree.set(neighbor, newDegree);
1301
+ if (newDegree === 0) {
1302
+ queue.push(neighbor);
1303
+ }
1304
+ }
1305
+ }
1306
+ if (result.length !== names.length) {
1307
+ const remaining = names.filter((n) => !result.includes(n));
1308
+ throw new Error(
1309
+ `Circular dependency detected among generators: ${remaining.join(", ")}`
1310
+ );
1311
+ }
1312
+ return result;
1313
+ }
1314
+ };
1315
+
1159
1316
  // src/plugins/manager.ts
1160
1317
  var nullLogger = {
1161
1318
  debug: () => {
@@ -1176,6 +1333,8 @@ var consoleLogger = {
1176
1333
  var PluginManager = class {
1177
1334
  _plugins = /* @__PURE__ */ new Map();
1178
1335
  _types = /* @__PURE__ */ new Map();
1336
+ _generators = /* @__PURE__ */ new Map();
1337
+ _pluginConfigs = /* @__PURE__ */ new Map();
1179
1338
  _cwd;
1180
1339
  _verbose;
1181
1340
  _logger;
@@ -1196,8 +1355,10 @@ var PluginManager = class {
1196
1355
  }
1197
1356
  /**
1198
1357
  * Registers a single plugin.
1358
+ * @param plugin - The plugin to register
1359
+ * @param pluginConfig - Optional configuration passed to plugin generators
1199
1360
  */
1200
- async register(plugin) {
1361
+ async register(plugin, pluginConfig = {}) {
1201
1362
  const warnings = [];
1202
1363
  const registeredTypes = [];
1203
1364
  if (this._plugins.has(plugin.name)) {
@@ -1209,6 +1370,7 @@ var PluginManager = class {
1209
1370
  };
1210
1371
  }
1211
1372
  this._logger.debug(`Registering plugin: ${plugin.name}@${plugin.version}`);
1373
+ this._pluginConfigs.set(plugin.name, pluginConfig);
1212
1374
  if (plugin.setup) {
1213
1375
  try {
1214
1376
  await plugin.setup(this.createContext());
@@ -1247,8 +1409,30 @@ var PluginManager = class {
1247
1409
  this._logger.debug(` Registered type: ${typeDef.name}`);
1248
1410
  }
1249
1411
  }
1412
+ if (plugin.generators) {
1413
+ for (const genDef of plugin.generators) {
1414
+ const existing = this._generators.get(genDef.name);
1415
+ if (existing) {
1416
+ warnings.push(
1417
+ `Generator '${genDef.name}' already registered by plugin '${existing.pluginName}'`
1418
+ );
1419
+ continue;
1420
+ }
1421
+ const registeredGenerator = {
1422
+ definition: genDef,
1423
+ pluginName: plugin.name,
1424
+ pluginVersion: plugin.version,
1425
+ pluginConfig
1426
+ };
1427
+ this._generators.set(genDef.name, registeredGenerator);
1428
+ this._logger.debug(` Registered generator: ${genDef.name}`);
1429
+ }
1430
+ }
1250
1431
  this._plugins.set(plugin.name, plugin);
1251
- this._logger.info(`Plugin registered: ${plugin.name} (${registeredTypes.length} types)`);
1432
+ const genCount = plugin.generators?.length ?? 0;
1433
+ this._logger.info(
1434
+ `Plugin registered: ${plugin.name} (${registeredTypes.length} types, ${genCount} generators)`
1435
+ );
1252
1436
  return {
1253
1437
  success: true,
1254
1438
  types: registeredTypes,
@@ -1299,7 +1483,7 @@ var PluginManager = class {
1299
1483
  return void 0;
1300
1484
  }
1301
1485
  /**
1302
- * Unregisters a plugin and its types.
1486
+ * Unregisters a plugin and its types/generators.
1303
1487
  */
1304
1488
  async unregister(pluginName) {
1305
1489
  const plugin = this._plugins.get(pluginName);
@@ -1319,6 +1503,12 @@ var PluginManager = class {
1319
1503
  this._types.delete(typeName);
1320
1504
  }
1321
1505
  }
1506
+ for (const [genName, gen] of this._generators) {
1507
+ if (gen.pluginName === pluginName) {
1508
+ this._generators.delete(genName);
1509
+ }
1510
+ }
1511
+ this._pluginConfigs.delete(pluginName);
1322
1512
  this._plugins.delete(pluginName);
1323
1513
  this._logger.info(`Plugin unregistered: ${pluginName}`);
1324
1514
  }
@@ -1358,17 +1548,57 @@ var PluginManager = class {
1358
1548
  getAllPlugins() {
1359
1549
  return this._plugins;
1360
1550
  }
1551
+ /**
1552
+ * Gets a registered generator by name.
1553
+ */
1554
+ getGenerator(generatorName) {
1555
+ return this._generators.get(generatorName);
1556
+ }
1557
+ /**
1558
+ * Checks if a generator is registered.
1559
+ */
1560
+ hasGenerator(generatorName) {
1561
+ return this._generators.has(generatorName);
1562
+ }
1563
+ /**
1564
+ * Gets all registered generators.
1565
+ */
1566
+ getAllGenerators() {
1567
+ return this._generators;
1568
+ }
1569
+ /**
1570
+ * Gets all registered generator names.
1571
+ */
1572
+ getGeneratorNames() {
1573
+ return Array.from(this._generators.keys());
1574
+ }
1361
1575
  /**
1362
1576
  * Gets the current registry state.
1363
1577
  */
1364
1578
  getRegistry() {
1365
1579
  return {
1366
1580
  plugins: this._plugins,
1367
- types: this._types
1581
+ types: this._types,
1582
+ generators: this._generators
1368
1583
  };
1369
1584
  }
1370
1585
  /**
1371
- * Clears all registered plugins and types.
1586
+ * Runs all registered generators in topological order.
1587
+ * @param schemas - The schemas to generate from
1588
+ * @returns Result with all generated outputs
1589
+ */
1590
+ async runGenerators(schemas) {
1591
+ const runner = new GeneratorRunner({
1592
+ cwd: this._cwd,
1593
+ logger: this._logger
1594
+ });
1595
+ for (const gen of this._generators.values()) {
1596
+ runner.register(gen);
1597
+ }
1598
+ return runner.runAll(schemas);
1599
+ }
1600
+ /**
1601
+ * Clears all registered plugins, types, and generators.
1372
1602
  */
1373
1603
  async clear() {
1374
1604
  for (const plugin of this._plugins.values()) {
@@ -1381,6 +1611,8 @@ var PluginManager = class {
1381
1611
  }
1382
1612
  this._plugins.clear();
1383
1613
  this._types.clear();
1614
+ this._generators.clear();
1615
+ this._pluginConfigs.clear();
1384
1616
  this._logger.debug("Plugin registry cleared");
1385
1617
  }
1386
1618
  };