@kinotic-ai/kinotic-cli 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 (82) hide show
  1. package/README.md +594 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +6 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +6 -0
  6. package/dist/commands/generate.d.ts +11 -0
  7. package/dist/commands/generate.js +36 -0
  8. package/dist/commands/initialize.d.ts +12 -0
  9. package/dist/commands/initialize.js +102 -0
  10. package/dist/commands/synchronize.d.ts +17 -0
  11. package/dist/commands/synchronize.js +154 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/internal/CodeGenerationService.d.ts +24 -0
  15. package/dist/internal/CodeGenerationService.js +256 -0
  16. package/dist/internal/Logger.d.ts +27 -0
  17. package/dist/internal/Logger.js +35 -0
  18. package/dist/internal/ProjectMigrationService.d.ts +28 -0
  19. package/dist/internal/ProjectMigrationService.js +99 -0
  20. package/dist/internal/Utils.d.ts +66 -0
  21. package/dist/internal/Utils.js +349 -0
  22. package/dist/internal/converter/ConverterConstants.d.ts +3 -0
  23. package/dist/internal/converter/ConverterConstants.js +4 -0
  24. package/dist/internal/converter/DefaultConversionContext.d.ts +29 -0
  25. package/dist/internal/converter/DefaultConversionContext.js +112 -0
  26. package/dist/internal/converter/IConversionContext.d.ts +64 -0
  27. package/dist/internal/converter/IConversionContext.js +11 -0
  28. package/dist/internal/converter/IConverterStrategy.d.ts +35 -0
  29. package/dist/internal/converter/IConverterStrategy.js +1 -0
  30. package/dist/internal/converter/ITypeConverter.d.ts +28 -0
  31. package/dist/internal/converter/ITypeConverter.js +1 -0
  32. package/dist/internal/converter/SpecificTypesConverter.d.ts +12 -0
  33. package/dist/internal/converter/SpecificTypesConverter.js +24 -0
  34. package/dist/internal/converter/codegen/ArrayC3TypeToStatementMapper.d.ts +9 -0
  35. package/dist/internal/converter/codegen/ArrayC3TypeToStatementMapper.js +46 -0
  36. package/dist/internal/converter/codegen/ObjectC3TypeToStatementMapper.d.ts +13 -0
  37. package/dist/internal/converter/codegen/ObjectC3TypeToStatementMapper.js +67 -0
  38. package/dist/internal/converter/codegen/PrimitiveC3TypeToStatementMapper.d.ts +9 -0
  39. package/dist/internal/converter/codegen/PrimitiveC3TypeToStatementMapper.js +24 -0
  40. package/dist/internal/converter/codegen/StatementMapper.d.ts +40 -0
  41. package/dist/internal/converter/codegen/StatementMapper.js +132 -0
  42. package/dist/internal/converter/codegen/StatementMapperConversionState.d.ts +9 -0
  43. package/dist/internal/converter/codegen/StatementMapperConversionState.js +17 -0
  44. package/dist/internal/converter/codegen/StatementMapperConverterStrategy.d.ts +16 -0
  45. package/dist/internal/converter/codegen/StatementMapperConverterStrategy.js +30 -0
  46. package/dist/internal/converter/codegen/UnionC3TypeToStatementMapper.d.ts +9 -0
  47. package/dist/internal/converter/codegen/UnionC3TypeToStatementMapper.js +46 -0
  48. package/dist/internal/converter/common/BaseConversionState.d.ts +9 -0
  49. package/dist/internal/converter/common/BaseConversionState.js +13 -0
  50. package/dist/internal/converter/typescript/ArrayToC3Type.d.ts +9 -0
  51. package/dist/internal/converter/typescript/ArrayToC3Type.js +17 -0
  52. package/dist/internal/converter/typescript/ConverterUtils.d.ts +4 -0
  53. package/dist/internal/converter/typescript/ConverterUtils.js +261 -0
  54. package/dist/internal/converter/typescript/EnumToC3Type.d.ts +12 -0
  55. package/dist/internal/converter/typescript/EnumToC3Type.js +26 -0
  56. package/dist/internal/converter/typescript/ObjectLikeToC3Type.d.ts +15 -0
  57. package/dist/internal/converter/typescript/ObjectLikeToC3Type.js +111 -0
  58. package/dist/internal/converter/typescript/PrimitiveToC3Type.d.ts +10 -0
  59. package/dist/internal/converter/typescript/PrimitiveToC3Type.js +33 -0
  60. package/dist/internal/converter/typescript/QueryOptionsToC3Type.d.ts +9 -0
  61. package/dist/internal/converter/typescript/QueryOptionsToC3Type.js +9 -0
  62. package/dist/internal/converter/typescript/TenantSelectionToC3Type.d.ts +9 -0
  63. package/dist/internal/converter/typescript/TenantSelectionToC3Type.js +9 -0
  64. package/dist/internal/converter/typescript/TypescriptConversionState.d.ts +24 -0
  65. package/dist/internal/converter/typescript/TypescriptConversionState.js +26 -0
  66. package/dist/internal/converter/typescript/TypescriptConverterStrategy.d.ts +16 -0
  67. package/dist/internal/converter/typescript/TypescriptConverterStrategy.js +44 -0
  68. package/dist/internal/converter/typescript/UnionToC3Type.d.ts +15 -0
  69. package/dist/internal/converter/typescript/UnionToC3Type.js +184 -0
  70. package/dist/internal/state/Environment.d.ts +13 -0
  71. package/dist/internal/state/Environment.js +65 -0
  72. package/dist/internal/state/IStateManager.d.ts +19 -0
  73. package/dist/internal/state/IStateManager.js +41 -0
  74. package/dist/internal/state/KinoticProjectConfigUtil.d.ts +9 -0
  75. package/dist/internal/state/KinoticProjectConfigUtil.js +153 -0
  76. package/dist/templates/AdminEntityService.liquid +14 -0
  77. package/dist/templates/BaseAdminEntityService.liquid +19 -0
  78. package/dist/templates/BaseEntityService.liquid +48 -0
  79. package/dist/templates/EntityService.liquid +14 -0
  80. package/dist/templates/KinoticProjectConfig.ts.liquid +10 -0
  81. package/oclif.manifest.json +161 -0
  82. package/package.json +97 -0
@@ -0,0 +1,102 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import chalk from 'chalk';
5
+ import { input } from '@inquirer/prompts';
6
+ import { isKinoticProject, saveKinoticProjectConfig } from '../internal/state/KinoticProjectConfigUtil.js';
7
+ import { KinoticProjectConfig } from '@kinotic-ai/core';
8
+ /**
9
+ * Validates the application name according to server requirements:
10
+ * - First character must be a letter
11
+ * - Can only contain letters, numbers, periods, underscores, or dashes
12
+ */
13
+ function validateApplicationName(name) {
14
+ if (!name || name.length === 0) {
15
+ return 'Application name cannot be empty';
16
+ }
17
+ // First character must be a letter
18
+ if (!/^[a-zA-Z]/.test(name)) {
19
+ return 'Application name must start with a letter';
20
+ }
21
+ // Can only contain letters, numbers, periods, underscores, or dashes
22
+ if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
23
+ return 'Application name can only contain letters, numbers, periods, underscores, or dashes';
24
+ }
25
+ return true;
26
+ }
27
+ export class Initialize extends Command {
28
+ static aliases = ['init'];
29
+ static description = 'This will initialize a new Kinotic Project for use with the Kinotic CLI.';
30
+ static examples = [
31
+ '$ kinotic initialize --application my.app --entities path/to/entities --generated path/to/services',
32
+ '$ kinotic init --application my.app --entities path/to/entities --generated path/to/services',
33
+ '$ kinotic init -a my.app -e path/to/entities -g path/to/services',
34
+ ];
35
+ static flags = {
36
+ application: Flags.string({ char: 'a', description: 'The name of the application you want to use', required: false }),
37
+ entities: Flags.string({ char: 'e', description: 'Path to the directory containing the Entity definitions', required: false }),
38
+ generated: Flags.string({ char: 'g', description: 'Path to the directory to write generated Services', required: false }),
39
+ };
40
+ async run() {
41
+ const { flags } = await this.parse(Initialize);
42
+ if (await isKinoticProject()) {
43
+ this.log(chalk.red('Error: ') + ' The working directory is already a Kinotic Project');
44
+ return;
45
+ }
46
+ // Prompt for missing values
47
+ let application = flags.application;
48
+ if (!application) {
49
+ application = await input({
50
+ message: 'What is the name of your application?',
51
+ validate: (input) => {
52
+ if (input.trim() === '') {
53
+ return 'Application name is required';
54
+ }
55
+ return validateApplicationName(input.trim());
56
+ }
57
+ });
58
+ }
59
+ else {
60
+ // Validate provided application name from flag
61
+ const validation = validateApplicationName(application);
62
+ if (validation !== true) {
63
+ this.error(validation);
64
+ }
65
+ }
66
+ let entitiesPath = flags.entities;
67
+ if (!entitiesPath) {
68
+ entitiesPath = await input({
69
+ message: 'Path to the directory containing Entity definitions:',
70
+ default: 'src/entities',
71
+ validate: (input) => input.trim() !== '' || 'Entities path is required'
72
+ });
73
+ }
74
+ let generatedPath = flags.generated;
75
+ if (!generatedPath) {
76
+ generatedPath = await input({
77
+ message: 'Path to the directory to write generated Services:',
78
+ default: 'src/generated',
79
+ validate: (input) => input.trim() !== '' || 'Generated path is required'
80
+ });
81
+ }
82
+ const entitiesAbsPath = path.resolve(entitiesPath);
83
+ const generatedAbsPath = path.resolve(generatedPath);
84
+ if (!fs.existsSync(entitiesAbsPath)) {
85
+ this.error(`Entities path does not exist: ${entitiesAbsPath}`);
86
+ }
87
+ if (!fs.existsSync(generatedAbsPath)) {
88
+ this.error(`Generated path does not exist: ${generatedAbsPath}`);
89
+ }
90
+ // Only use TypescriptProjectConfig for initialization
91
+ const configDir = path.resolve(process.cwd(), '.config');
92
+ const configObj = new KinoticProjectConfig();
93
+ // Don't set name - it will be loaded from package.json
94
+ configObj.application = application;
95
+ configObj.entitiesPaths = [entitiesPath];
96
+ configObj.generatedPath = generatedPath;
97
+ configObj.validate = false;
98
+ configObj.fileExtensionForImports = '.js';
99
+ await saveKinoticProjectConfig(configObj, configDir);
100
+ this.log(chalk.green('Success:') + ' Initialized Project');
101
+ }
102
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ export declare class Synchronize extends Command {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ server: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ publish: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
+ verbose: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
+ authHeaderFile: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ dryRun: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ logVerbose(message: string | (() => string), verbose: boolean): void;
15
+ private synchronizeEntity;
16
+ private synchronizeNamedQueries;
17
+ }
@@ -0,0 +1,154 @@
1
+ import { isKinoticProject, loadKinoticProjectConfig } from '../internal/state/KinoticProjectConfigUtil.js';
2
+ import { Kinotic } from '@kinotic-ai/core';
3
+ import { EntityDefinition, NamedQueriesDefinition, OsApiPlugin, Project, ProjectType } from '@kinotic-ai/os-api';
4
+ import { Command, Flags } from '@oclif/core';
5
+ import chalk from 'chalk';
6
+ import { WebSocket } from 'ws';
7
+ import { CodeGenerationService } from '../internal/CodeGenerationService.js';
8
+ import { ProjectMigrationService } from '../internal/ProjectMigrationService.js';
9
+ import { resolveServer } from '../internal/state/Environment.js';
10
+ import { connectAndUpgradeSession } from '../internal/Utils.js';
11
+ // This is required when running Kinotic from node
12
+ Object.assign(global, { WebSocket });
13
+ Kinotic.use(OsApiPlugin);
14
+ export class Synchronize extends Command {
15
+ static aliases = ['sync'];
16
+ static description = 'Synchronize the local Entity definitions with the Kinotic Server';
17
+ static examples = [
18
+ '$ kinotic synchronize',
19
+ '$ kinotic sync',
20
+ '$ kinotic synchronize --server http://localhost:9090 --publish --verbose',
21
+ '$ kinotic sync -p -v -s http://localhost:9090'
22
+ ];
23
+ static flags = {
24
+ server: Flags.string({ char: 's', description: 'The Kinotic server to connect to' }),
25
+ publish: Flags.boolean({ char: 'p', description: 'Publish each Entity after save/update' }),
26
+ verbose: Flags.boolean({ char: 'v', description: 'Enable verbose logging' }),
27
+ authHeaderFile: Flags.string({ char: 'f', description: 'JSON File containing authentication headers', required: false }),
28
+ dryRun: Flags.boolean({ description: 'Dry run enables verbose logging and does not save any changes to the server' })
29
+ };
30
+ async run() {
31
+ const { flags } = await this.parse(Synchronize);
32
+ try {
33
+ if (!(await isKinoticProject())) {
34
+ this.error('The working directory is not a Kinotic Project');
35
+ }
36
+ const kinoticProjectConfig = await loadKinoticProjectConfig();
37
+ let serverUrl = '';
38
+ if (!flags.dryRun) {
39
+ const serverConfig = await resolveServer(this.config.configDir, flags.server);
40
+ serverUrl = serverConfig.url;
41
+ }
42
+ if (flags.dryRun || await connectAndUpgradeSession(serverUrl, this, flags.authHeaderFile)) {
43
+ try {
44
+ let project = null;
45
+ if (!flags.dryRun) {
46
+ await Kinotic.applications.createApplicationIfNotExist(kinoticProjectConfig.application, '');
47
+ project = new Project(null, kinoticProjectConfig.application, kinoticProjectConfig.name, kinoticProjectConfig.description);
48
+ project.sourceOfTruth = ProjectType.TYPESCRIPT;
49
+ project = await Kinotic.projects.createProjectIfNotExist(project);
50
+ }
51
+ const codeGenerationService = new CodeGenerationService(kinoticProjectConfig.application, kinoticProjectConfig.fileExtensionForImports, this);
52
+ await codeGenerationService
53
+ .generateAllEntities(kinoticProjectConfig, flags.verbose || flags.dryRun, async (entityInfo, services) => {
54
+ // combine named queries from generated services
55
+ const namedQueries = [];
56
+ for (let serviceInfo of services) {
57
+ namedQueries.push(...serviceInfo.namedQueries);
58
+ }
59
+ // We sync named queries first since currently the backend cache eviction logic is a little dumb
60
+ // i.e. The cache eviction for the structure deletes the GraphQL schema
61
+ // This will evict the named query execution plan cache
62
+ // We want to make sure the GraphQL schema is updated after both these are updated and the structure below
63
+ if (!flags.dryRun && namedQueries.length > 0) {
64
+ await this.synchronizeNamedQueries(project.id, entityInfo.entity, namedQueries);
65
+ }
66
+ if (!flags.dryRun) {
67
+ await this.synchronizeEntity(project.id, entityInfo.entity, flags.publish, flags.verbose);
68
+ }
69
+ });
70
+ // Apply migrations after entity synchronization
71
+ if (!flags.dryRun) {
72
+ const migrationService = new ProjectMigrationService(this);
73
+ await migrationService.applyMigrations(project.id, './migrations', flags.verbose);
74
+ }
75
+ this.log(`Synchronization Complete For application: ${kinoticProjectConfig.application}`);
76
+ }
77
+ catch (e) {
78
+ if (e instanceof Error) {
79
+ this.error(e.message);
80
+ }
81
+ }
82
+ }
83
+ await Kinotic.disconnect();
84
+ }
85
+ catch (e) {
86
+ if (e instanceof Error) {
87
+ this.log(chalk.red('Error: ') + e.message);
88
+ }
89
+ else {
90
+ this.log(chalk.red('Error: ') + e);
91
+ }
92
+ await Kinotic.disconnect();
93
+ }
94
+ return;
95
+ }
96
+ logVerbose(message, verbose) {
97
+ if (verbose) {
98
+ if (typeof message === 'function') {
99
+ this.log(message());
100
+ }
101
+ else {
102
+ this.log(message);
103
+ }
104
+ }
105
+ }
106
+ async synchronizeEntity(projectId, entity, publish, verbose) {
107
+ const entityDefinitionService = Kinotic.entityDefinitions;
108
+ const application = entity.namespace;
109
+ const name = entity.name;
110
+ const structureId = (application + '.' + name).toLowerCase();
111
+ this.log(`Synchronizing Entity: ${application}.${name}`);
112
+ try {
113
+ let structure = await entityDefinitionService.findById(structureId);
114
+ if (structure) {
115
+ if (structure.published) {
116
+ this.log(chalk.bold(`Entity ${chalk.blue(application + '.' + name)} is Published. ${chalk.yellow('(Supported Modifications: New Fields. Un-Publish for all other changes.)')}`));
117
+ }
118
+ // update existing entity
119
+ structure.entityDefinition = entity;
120
+ this.logVerbose(`Updating Entity: ${application}.${name}`, verbose);
121
+ structure = await entityDefinitionService.save(structure);
122
+ }
123
+ else {
124
+ structure = new EntityDefinition(application, projectId, name, entity);
125
+ this.logVerbose(`Creating Entity: ${application}.${name}`, verbose);
126
+ structure = await entityDefinitionService.create(structure);
127
+ }
128
+ // publish if we need to
129
+ if (!structure.published && publish && structure?.id) {
130
+ this.logVerbose(`Publishing Entity: ${application}.${name}`, verbose);
131
+ await entityDefinitionService.publish(structure.id);
132
+ }
133
+ }
134
+ catch (e) {
135
+ const message = e?.message || e;
136
+ this.log(chalk.red('Error') + ` Synchronizing Entity: ${structureId}, Exception: ${message}`);
137
+ }
138
+ }
139
+ async synchronizeNamedQueries(projectId, entity, namedQueries) {
140
+ const namedQueriesService = Kinotic.namedQueriesDefinitions;
141
+ const application = entity.namespace;
142
+ const structure = entity.name;
143
+ const id = (application + '.' + structure).toLowerCase();
144
+ this.log(`Synchronizing Named Queries for Entity: ${application}.${structure}`);
145
+ try {
146
+ const namedQueriesDefinition = new NamedQueriesDefinition(id, application, projectId, structure, namedQueries);
147
+ await namedQueriesService.save(namedQueriesDefinition);
148
+ }
149
+ catch (e) {
150
+ const message = e?.message || e;
151
+ this.log(chalk.red('Error') + ` Synchronizing Named Queries for Entity: ${id}, Exception: ${message}`);
152
+ }
153
+ }
154
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,24 @@
1
+ import { KinoticProjectConfig } from '@kinotic-ai/core';
2
+ import { Logger } from './Logger.js';
3
+ import { EntityInfo, GeneratedServiceInfo } from './Utils.js';
4
+ export type GeneratedEntityProcessor = (entityInfo: EntityInfo, serviceInfo: GeneratedServiceInfo[]) => Promise<void>;
5
+ /**
6
+ * Helper service for generating code.s
7
+ */
8
+ export declare class CodeGenerationService {
9
+ private readonly fileExtensionForImports;
10
+ private readonly logger;
11
+ private readonly engine;
12
+ private readonly tsMorphProject;
13
+ private readonly conversionContext;
14
+ constructor(application: string, fileExtensionForImports: string, logger: Logger);
15
+ generateAllEntities(projectConfig: KinoticProjectConfig, verbose: boolean, entityProcessor?: GeneratedEntityProcessor): Promise<void>;
16
+ private processEntities;
17
+ private generateEntityService;
18
+ /**
19
+ * Adds invocation logic to named queries and returns the {@link FunctionDefinition}s that define them.
20
+ */
21
+ private processNamedQueries;
22
+ private createC3TypeForReturnType;
23
+ private createStatementMapper;
24
+ }
@@ -0,0 +1,256 @@
1
+ import { FunctionDefinition, ObjectC3Type } from '@kinotic-ai/idl';
2
+ import fs from 'fs';
3
+ import { Liquid } from 'liquidjs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { PageableC3Type, PageC3Type } from '@kinotic-ai/os-api';
7
+ import { createImportString } from './converter/codegen/StatementMapper.js';
8
+ import { StatementMapperConversionState } from './converter/codegen/StatementMapperConversionState.js';
9
+ import { StatementMapperConverterStrategy } from './converter/codegen/StatementMapperConverterStrategy.js';
10
+ import { createConversionContext } from './converter/IConversionContext.js';
11
+ import { tsDecoratorToC3Decorator } from './converter/typescript/ConverterUtils.js';
12
+ import { TypescriptConversionState } from './converter/typescript/TypescriptConversionState.js';
13
+ import { TypescriptConverterStrategy } from './converter/typescript/TypescriptConverterStrategy.js';
14
+ import { convertAllEntities, createTsMorphProject, getRelativeImportPath, tryGetNodeModuleName, writeEntityJsonToFilesystem, writeGeneratedServiceInfoToFilesystem } from './Utils.js';
15
+ import chalk from 'chalk';
16
+ /**
17
+ * Helper service for generating code.s
18
+ */
19
+ export class CodeGenerationService {
20
+ fileExtensionForImports;
21
+ logger;
22
+ engine;
23
+ tsMorphProject;
24
+ conversionContext;
25
+ constructor(application, fileExtensionForImports, logger) {
26
+ this.fileExtensionForImports = fileExtensionForImports;
27
+ this.logger = logger;
28
+ this.engine = new Liquid({
29
+ root: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../templates/'), // root for templates lookup
30
+ extname: '.liquid'
31
+ });
32
+ this.tsMorphProject = createTsMorphProject();
33
+ const state = new TypescriptConversionState(application);
34
+ state.shouldAddSourcePathToMetadata = false;
35
+ this.conversionContext = createConversionContext(new TypescriptConverterStrategy(state, logger));
36
+ }
37
+ async generateAllEntities(projectConfig, verbose, entityProcessor) {
38
+ for (const entitiesPath of projectConfig.entitiesPaths) {
39
+ const config = {
40
+ application: projectConfig.application,
41
+ entitiesPath: entitiesPath,
42
+ verbose: verbose,
43
+ logger: this.logger
44
+ };
45
+ await this.processEntities(config, projectConfig, entityProcessor);
46
+ }
47
+ }
48
+ async processEntities(config, projectConfig, entityProcessor) {
49
+ if (!fs.existsSync(config.entitiesPath)) {
50
+ throw new Error(`Entities path does not exist: ${config.entitiesPath}`);
51
+ }
52
+ const convertedEntities = convertAllEntities(config);
53
+ if (convertedEntities.length > 0) {
54
+ for (const entityInfo of convertedEntities) {
55
+ this.logger.logVerbose(`Generated Structure Mapping for ${entityInfo.entity.namespace}.${entityInfo.entity.name}`, config.verbose);
56
+ const generatedServices = [];
57
+ generatedServices.push(await this.generateEntityService(false, entityInfo, projectConfig));
58
+ if (entityInfo.multiTenantSelectionEnabled) {
59
+ generatedServices.push(await this.generateEntityService(true, entityInfo, projectConfig));
60
+ }
61
+ if (config.verbose) {
62
+ await writeEntityJsonToFilesystem(projectConfig.generatedPath, entityInfo.entity, this.logger);
63
+ for (let generatedServiceInfo of generatedServices) {
64
+ if (generatedServiceInfo.namedQueries.length > 0) {
65
+ await writeGeneratedServiceInfoToFilesystem(projectConfig.generatedPath, generatedServiceInfo, this.logger);
66
+ }
67
+ }
68
+ }
69
+ if (entityProcessor) {
70
+ await entityProcessor(entityInfo, generatedServices);
71
+ }
72
+ }
73
+ }
74
+ else {
75
+ this.logger.logVerbose(`No Entities found For namespace: ${config.application} and Entities Path: ${config.entitiesPath}`, config.verbose);
76
+ }
77
+ }
78
+ async generateEntityService(adminService, entityInfo, projectConfig) {
79
+ const adminPrefix = (adminService ? 'Admin' : '');
80
+ const fileExtensionForImports = this.fileExtensionForImports;
81
+ const generatedPath = projectConfig.generatedPath;
82
+ const baseEntityServicePath = path.resolve(generatedPath, 'generated', `Base${entityInfo.entity.name}${adminPrefix}EntityService.ts`);
83
+ const entityServicePath = path.resolve(generatedPath, `${entityInfo.entity.name}${adminPrefix}EntityService.ts`);
84
+ const entityName = entityInfo.entity.name;
85
+ const entityNamespace = entityInfo.entity.namespace;
86
+ const defaultExport = entityInfo.defaultExport;
87
+ const validate = projectConfig.validate;
88
+ let entityImportPath = tryGetNodeModuleName(entityInfo.exportedFromFile);
89
+ if (!entityImportPath) {
90
+ entityImportPath = getRelativeImportPath(baseEntityServicePath, entityInfo.exportedFromFile, fileExtensionForImports);
91
+ }
92
+ // We don't add validation logic to admin services since there are no save/update methods there
93
+ let statement = null;
94
+ let validationLogic = null;
95
+ if (!adminService) {
96
+ statement = this.createStatementMapper(entityInfo);
97
+ validationLogic = statement.toStatementString();
98
+ }
99
+ const importStatements = createImportString(statement, baseEntityServicePath, fileExtensionForImports) || '';
100
+ // We always generate the base entity service. This way if our internal logic changes we can update it
101
+ fs.mkdirSync(path.dirname(baseEntityServicePath), { recursive: true });
102
+ const baseReadStream = await this.engine.renderFileToNodeStream(`Base${adminPrefix}EntityService`, {
103
+ entityName,
104
+ entityNamespace,
105
+ defaultExport,
106
+ entityImportPath,
107
+ validationLogic,
108
+ importStatements
109
+ });
110
+ let baseWriteStream = fs.createWriteStream(baseEntityServicePath);
111
+ baseReadStream.pipe(baseWriteStream);
112
+ // we only generate if the file does not exist
113
+ let namedQueries = [];
114
+ if (!fs.existsSync(entityServicePath)) {
115
+ const readStream = await this.engine.renderFileToNodeStream(`${adminPrefix}EntityService`, {
116
+ entityName,
117
+ entityNamespace,
118
+ validate,
119
+ fileExtensionForImports
120
+ });
121
+ let writeStream = fs.createWriteStream(entityServicePath);
122
+ readStream.pipe(writeStream);
123
+ }
124
+ else {
125
+ // if it already exists we check if there are any named queries defined
126
+ namedQueries = await this.processNamedQueries(`${entityName}${adminPrefix}EntityService`, entityServicePath);
127
+ }
128
+ return {
129
+ entityServiceName: `${entityName}${adminPrefix}EntityService`,
130
+ namedQueries: namedQueries
131
+ };
132
+ }
133
+ /**
134
+ * Adds invocation logic to named queries and returns the {@link FunctionDefinition}s that define them.
135
+ */
136
+ async processNamedQueries(entityServiceName, entityServicePath) {
137
+ const namedQueries = [];
138
+ this.tsMorphProject.addSourceFileAtPath(entityServicePath);
139
+ const entityServiceSource = this.tsMorphProject.getSourceFile(entityServicePath);
140
+ // Currently we only support named queries when added to the generated entity service class
141
+ if (entityServiceSource) {
142
+ const serviceClass = entityServiceSource.getClass(entityServiceName);
143
+ if (serviceClass) {
144
+ // Convert all methods that have a @Query decorator to a C3 FunctionDefinition
145
+ // These will be used to define the named queries for the structure
146
+ for (const method of serviceClass?.getInstanceMethods()) {
147
+ const queryDecorator = method.getDecorator('Query');
148
+ if (queryDecorator) {
149
+ const methodName = method.getName();
150
+ const functionDefinition = new FunctionDefinition(methodName, [tsDecoratorToC3Decorator(queryDecorator)]);
151
+ // TODO: add more generic decorator handling
152
+ const policyDecorator = method.getDecorator('Policy');
153
+ if (policyDecorator) {
154
+ functionDefinition.addDecorator(tsDecoratorToC3Decorator(policyDecorator));
155
+ }
156
+ functionDefinition.returnType = this.createC3TypeForReturnType(method.getReturnType());
157
+ // Find page parameter if any and store all parameter names for later
158
+ const argNames = [];
159
+ let pageableParameterName = null;
160
+ const parameters = method.getParameters();
161
+ for (let i = 0; i < parameters.length; i++) {
162
+ let parameter = parameters[i];
163
+ const parameterName = parameter.getName();
164
+ const parameterTypeName = parameter.getType().getSymbol()?.getName();
165
+ let parameterC3Type;
166
+ if (parameterTypeName === 'Pageable'
167
+ || parameterTypeName === 'OffsetPageable'
168
+ || parameterTypeName === 'CursorPageable') {
169
+ if (i > 0 && i < parameters.length - 1) {
170
+ this.logger.log(chalk.yellow(`It is best if Pageable is always the first or last parameter.`));
171
+ }
172
+ pageableParameterName = parameterName;
173
+ parameterC3Type = new PageableC3Type();
174
+ }
175
+ else {
176
+ argNames.push(parameterName);
177
+ parameterC3Type = this.conversionContext.convert(parameter.getType());
178
+ }
179
+ functionDefinition.addParameter(parameterName, parameterC3Type);
180
+ }
181
+ // Code generated is similar to the following
182
+ // const parameters = [
183
+ // {key: 'who', value: who},
184
+ // {key: 'howMany', value: howMany}
185
+ // ]
186
+ // return this.namedQuery('namedQueryTest', parameters)
187
+ method.setBodyText(writer => {
188
+ writer.writeLine('/** Autogenerated code, changes will be overwritten. **/');
189
+ if (argNames.length > 0) {
190
+ writer.writeLine('const parameters = [')
191
+ .indent(() => {
192
+ for (let i = 0; i < argNames.length; i++) {
193
+ let argName = argNames[i];
194
+ writer.write(`{key: '${argName}', value: ${argName}}`);
195
+ if (i != argNames.length - 1) {
196
+ writer.write(',');
197
+ }
198
+ writer.newLine();
199
+ }
200
+ })
201
+ .writeLine(']');
202
+ }
203
+ if (pageableParameterName) {
204
+ writer.writeLine(`return this.namedQueryPage('${methodName}', ${argNames.length > 0 ? 'parameters' : '[]'}, ${pageableParameterName})`);
205
+ }
206
+ else {
207
+ writer.writeLine(`return this.namedQuery('${methodName}', ${argNames.length > 0 ? 'parameters' : '[]'})`);
208
+ }
209
+ });
210
+ namedQueries.push(functionDefinition);
211
+ }
212
+ }
213
+ await entityServiceSource.save();
214
+ }
215
+ }
216
+ return namedQueries;
217
+ }
218
+ createC3TypeForReturnType(returnType) {
219
+ let ret;
220
+ // All methods must return a Promise
221
+ if (returnType.getSymbol()?.getName() === 'Promise') {
222
+ let typeArguments = returnType.getTypeArguments();
223
+ if (typeArguments?.length !== 1) {
224
+ throw new Error('Promise must have exactly one type argument');
225
+ }
226
+ returnType = typeArguments[0];
227
+ // If a Page is being returned then use internal PageC3Type for simplicity
228
+ if (returnType.getSymbol()?.getName() === 'Page'
229
+ || returnType.getSymbol()?.getName() === 'IterablePage') {
230
+ typeArguments = returnType.getTypeArguments();
231
+ if (typeArguments?.length !== 1) {
232
+ throw new Error('Page must have exactly one type argument');
233
+ }
234
+ const contentType = this.conversionContext.convert(typeArguments[0]);
235
+ if (contentType instanceof ObjectC3Type) {
236
+ ret = new PageC3Type(contentType);
237
+ }
238
+ else {
239
+ throw new Error('Only Object types are supported for Page content types');
240
+ }
241
+ }
242
+ else {
243
+ ret = this.conversionContext.convert(returnType);
244
+ }
245
+ }
246
+ else {
247
+ throw new Error('Only methods that return a Promise are supported for named queries');
248
+ }
249
+ return ret;
250
+ }
251
+ createStatementMapper(entityInfo) {
252
+ const state = new StatementMapperConversionState(entityInfo.entity.namespace);
253
+ const conversionContext = createConversionContext(new StatementMapperConverterStrategy(state, this.logger));
254
+ return conversionContext.convert(entityInfo.entity);
255
+ }
256
+ }
@@ -0,0 +1,27 @@
1
+ import { PrettyPrintableError } from '@oclif/core/lib/errors';
2
+ export type Logger = {
3
+ log: (message?: string, ...args: any[]) => void;
4
+ warn(input: string | Error): string | Error;
5
+ error(input: string | Error, options: {
6
+ code?: string;
7
+ exit: false;
8
+ } & PrettyPrintableError): void;
9
+ error(input: string | Error, options?: {
10
+ code?: string;
11
+ exit?: number;
12
+ } & PrettyPrintableError): never;
13
+ logVerbose(message: string | (() => string), verbose: boolean): void;
14
+ };
15
+ export declare class ConsoleLogger implements Logger {
16
+ log(message?: string, ...args: any[]): void;
17
+ warn(input: string | Error): string | Error;
18
+ error(input: string | Error, options: {
19
+ code?: string;
20
+ exit: false;
21
+ } & PrettyPrintableError): void;
22
+ error(input: string | Error, options?: {
23
+ code?: string;
24
+ exit?: number;
25
+ } & PrettyPrintableError): never;
26
+ logVerbose(message: string | (() => string), verbose: boolean): void;
27
+ }
@@ -0,0 +1,35 @@
1
+ export class ConsoleLogger {
2
+ log(message, ...args) {
3
+ console.log(message, ...args);
4
+ }
5
+ warn(input) {
6
+ const message = input instanceof Error ? input.message : input;
7
+ console.warn(message);
8
+ return input;
9
+ }
10
+ error(input, options) {
11
+ const message = input instanceof Error ? input.message : input;
12
+ console.error('Error:', message);
13
+ if (options) {
14
+ if (options.code) {
15
+ console.error('Code:', options.code);
16
+ }
17
+ if (options.ref) {
18
+ console.error('Reference:', options.ref);
19
+ }
20
+ if (options.suggestions && options.suggestions.length > 0) {
21
+ console.error('Suggestions:');
22
+ options.suggestions.forEach((suggestion) => console.error(`- ${suggestion}`));
23
+ }
24
+ }
25
+ if (options?.exit !== undefined && options.exit !== false) {
26
+ process.exit(typeof options.exit === 'number' ? options.exit : 1);
27
+ }
28
+ }
29
+ logVerbose(message, verbose) {
30
+ if (verbose) {
31
+ const output = typeof message === 'function' ? message() : message;
32
+ console.log(output);
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,28 @@
1
+ import { Logger } from './Logger.js';
2
+ /**
3
+ * Internal service for loading and applying migrations from the local filesystem.
4
+ * Looks for migration files in a 'migrations' folder in the project root.
5
+ */
6
+ export declare class ProjectMigrationService {
7
+ private readonly migrationService;
8
+ private readonly logger;
9
+ constructor(logger: Logger);
10
+ /**
11
+ * Loads migration files from the migrations directory and applies any new ones.
12
+ *
13
+ * @param projectId the project identifier to apply migrations to
14
+ * @param migrationsDir the directory containing migration files (defaults to './migrations')
15
+ * @param verbose whether to enable verbose logging
16
+ */
17
+ applyMigrations(projectId: string, migrationsDir?: string, verbose?: boolean): Promise<void>;
18
+ /**
19
+ * Loads only the migration files that need to be applied, checking everything in one pass.
20
+ */
21
+ private loadUnappliedMigrations;
22
+ /**
23
+ * Extracts the version number from a migration filename.
24
+ * Expected format: V{version}__{description}.sql
25
+ */
26
+ private extractVersionFromFilename;
27
+ private logVerbose;
28
+ }