@intrig/core 0.0.15-5 → 0.0.15-6
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/main.js +869 -173
- package/package.json +2 -1
package/main.js
CHANGED
|
@@ -981,6 +981,8 @@ SourcesCommand = sources_command_ts_decorate([
|
|
|
981
981
|
})
|
|
982
982
|
], SourcesCommand);
|
|
983
983
|
|
|
984
|
+
;// external "proper-lockfile"
|
|
985
|
+
const external_proper_lockfile_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("proper-lockfile");
|
|
984
986
|
;// external "lodash"
|
|
985
987
|
const external_lodash_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("lodash");
|
|
986
988
|
var external_lodash_default = /*#__PURE__*/__webpack_require__.n(external_lodash_namespaceObject);
|
|
@@ -1288,30 +1290,110 @@ function spec_management_service_ts_metadata(k, v) {
|
|
|
1288
1290
|
|
|
1289
1291
|
|
|
1290
1292
|
|
|
1293
|
+
|
|
1291
1294
|
class SpecManagementService {
|
|
1292
1295
|
constructor(config){
|
|
1293
1296
|
this.config = config;
|
|
1297
|
+
this.logger = new common_namespaceObject.Logger(SpecManagementService.name);
|
|
1294
1298
|
this.specsDir = this.config.get('specsDir') ?? external_path_namespaceObject.resolve(external_node_process_namespaceObject.cwd(), '.intrig', 'specs');
|
|
1295
1299
|
}
|
|
1296
1300
|
async save(apiName, content) {
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1301
|
+
const filePath = external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json`);
|
|
1302
|
+
const tempPath = external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json.tmp`);
|
|
1303
|
+
let release;
|
|
1304
|
+
try {
|
|
1305
|
+
// Ensure directory exists
|
|
1306
|
+
external_fs_namespaceObject.mkdirSync(this.specsDir, {
|
|
1307
|
+
recursive: true
|
|
1308
|
+
});
|
|
1309
|
+
// Acquire lock for the file
|
|
1310
|
+
release = await external_proper_lockfile_namespaceObject.lock(filePath, {
|
|
1311
|
+
retries: {
|
|
1312
|
+
retries: 5,
|
|
1313
|
+
factor: 2,
|
|
1314
|
+
minTimeout: 100,
|
|
1315
|
+
maxTimeout: 2000
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
// Read current content with lock protection
|
|
1319
|
+
const currentContent = await this.readWithoutLock(apiName);
|
|
1320
|
+
if (currentContent) {
|
|
1321
|
+
const differences = openapi3_diff(currentContent, JSON.parse(content));
|
|
1322
|
+
if (!Object.keys(differences).length) {
|
|
1323
|
+
this.logger.debug(`No changes detected for ${apiName}, skipping save`);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
// Atomic write: write to temp file first, then rename
|
|
1328
|
+
external_fs_namespaceObject.writeFileSync(tempPath, content, 'utf-8');
|
|
1329
|
+
external_fs_namespaceObject.renameSync(tempPath, filePath);
|
|
1330
|
+
this.logger.debug(`Successfully saved spec for ${apiName}`);
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
// Clean up temp file if it exists
|
|
1333
|
+
if (external_fs_namespaceObject.existsSync(tempPath)) {
|
|
1334
|
+
try {
|
|
1335
|
+
external_fs_namespaceObject.unlinkSync(tempPath);
|
|
1336
|
+
} catch (cleanupError) {
|
|
1337
|
+
this.logger.warn(`Failed to cleanup temp file ${tempPath}: ${cleanupError}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
this.logger.error(`Failed to save spec for ${apiName}: ${error.message}`, error);
|
|
1341
|
+
throw new Error(`Failed to save spec for ${apiName}: ${error.message}`);
|
|
1342
|
+
} finally{
|
|
1343
|
+
// Always release the lock
|
|
1344
|
+
if (release) {
|
|
1345
|
+
try {
|
|
1346
|
+
await release();
|
|
1347
|
+
} catch (releaseError) {
|
|
1348
|
+
this.logger.warn(`Failed to release lock for ${filePath}: ${releaseError}`);
|
|
1349
|
+
}
|
|
1302
1350
|
}
|
|
1303
1351
|
}
|
|
1304
|
-
external_fs_namespaceObject.mkdirSync(this.specsDir, {
|
|
1305
|
-
recursive: true
|
|
1306
|
-
});
|
|
1307
|
-
external_fs_namespaceObject.writeFileSync(external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json`), content, 'utf-8');
|
|
1308
1352
|
}
|
|
1309
1353
|
async read(apiName) {
|
|
1354
|
+
const filePath = external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json`);
|
|
1355
|
+
let release;
|
|
1356
|
+
try {
|
|
1357
|
+
// Acquire lock for reading
|
|
1358
|
+
release = await external_proper_lockfile_namespaceObject.lock(filePath, {
|
|
1359
|
+
retries: {
|
|
1360
|
+
retries: 3,
|
|
1361
|
+
factor: 2,
|
|
1362
|
+
minTimeout: 50,
|
|
1363
|
+
maxTimeout: 1000
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
return await this.readWithoutLock(apiName);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (error.code === 'ENOENT' || !external_fs_namespaceObject.existsSync(filePath)) {
|
|
1369
|
+
// File doesn't exist, this is normal for first-time sync
|
|
1370
|
+
this.logger.debug(`Spec file not found for ${apiName}, returning undefined`);
|
|
1371
|
+
return undefined;
|
|
1372
|
+
}
|
|
1373
|
+
this.logger.error(`Failed to read spec for ${apiName}: ${error.message}`, error);
|
|
1374
|
+
throw new Error(`Failed to read spec for ${apiName}: ${error.message}`);
|
|
1375
|
+
} finally{
|
|
1376
|
+
// Always release the lock
|
|
1377
|
+
if (release) {
|
|
1378
|
+
try {
|
|
1379
|
+
await release();
|
|
1380
|
+
} catch (releaseError) {
|
|
1381
|
+
this.logger.warn(`Failed to release lock for ${filePath}: ${releaseError}`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
async readWithoutLock(apiName) {
|
|
1310
1387
|
const fileName = `${apiName}-latest.json`;
|
|
1311
1388
|
const specPath = external_path_namespaceObject.join(this.specsDir, fileName);
|
|
1312
1389
|
if (external_fs_namespaceObject.existsSync(specPath)) {
|
|
1313
|
-
|
|
1314
|
-
|
|
1390
|
+
try {
|
|
1391
|
+
const content = external_fs_namespaceObject.readFileSync(specPath, 'utf-8');
|
|
1392
|
+
return JSON.parse(content);
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
this.logger.error(`Failed to parse JSON for ${apiName}: ${error.message}`);
|
|
1395
|
+
throw new Error(`Invalid JSON in spec file for ${apiName}: ${error.message}`);
|
|
1396
|
+
}
|
|
1315
1397
|
}
|
|
1316
1398
|
return undefined;
|
|
1317
1399
|
}
|
|
@@ -5535,77 +5617,526 @@ if (isPending(${camelCase(descriptor.name)}Resp)) {
|
|
|
5535
5617
|
${"```"}
|
|
5536
5618
|
</details>
|
|
5537
5619
|
|
|
5538
|
-
<details>
|
|
5539
|
-
<summary>Inline rendering</summary>
|
|
5540
|
-
If you need to show a non-blocking/parallel loading indicator, or change the component behavior based on the loading state, you can inline the loading condition.
|
|
5541
|
-
|
|
5542
|
-
${"```tsx"}
|
|
5543
|
-
<>{isPending(${camelCase(descriptor.name)}Resp) ? <LoadingIndicator /> : null}<MyComponent /></>
|
|
5544
|
-
${"```"}
|
|
5545
|
-
</details>
|
|
5620
|
+
<details>
|
|
5621
|
+
<summary>Inline rendering</summary>
|
|
5622
|
+
If you need to show a non-blocking/parallel loading indicator, or change the component behavior based on the loading state, you can inline the loading condition.
|
|
5623
|
+
|
|
5624
|
+
${"```tsx"}
|
|
5625
|
+
<>{isPending(${camelCase(descriptor.name)}Resp) ? <LoadingIndicator /> : null}<MyComponent /></>
|
|
5626
|
+
${"```"}
|
|
5627
|
+
</details>
|
|
5628
|
+
|
|
5629
|
+
#### Error state
|
|
5630
|
+
The error state extraction is done using the \`isError\` utility method. Make sure to import the method into your component.
|
|
5631
|
+
|
|
5632
|
+
${"```tsx"}
|
|
5633
|
+
import { isError } from '@intrig/next';
|
|
5634
|
+
${"```"}
|
|
5635
|
+
|
|
5636
|
+
<details>
|
|
5637
|
+
<summary>Early return</summary>
|
|
5638
|
+
In most cases, in an error state, you may want to show the error message to the user instead of real component. In that case, you can add a conditional early return.
|
|
5639
|
+
|
|
5640
|
+
${"```tsx"}
|
|
5641
|
+
if (isError(${camelCase(descriptor.name)}Resp)) {
|
|
5642
|
+
return <ErrorMessage />
|
|
5643
|
+
}
|
|
5644
|
+
${"```"}
|
|
5645
|
+
</details>
|
|
5646
|
+
|
|
5647
|
+
<details>
|
|
5648
|
+
<summary>Inline rendering</summary>
|
|
5649
|
+
If you need to show a non-blocking/parallel error indicator, or change the component behavior based on the error state, you can inline the error condition.
|
|
5650
|
+
|
|
5651
|
+
${"```tsx"}
|
|
5652
|
+
<>{isError(${camelCase(descriptor.name)}Resp) ? <ErrorMessage /> : null}<MyComponent /></>
|
|
5653
|
+
${"```"}
|
|
5654
|
+
</details>
|
|
5655
|
+
|
|
5656
|
+
<details>
|
|
5657
|
+
<summary>Side effect upon error</summary>
|
|
5658
|
+
|
|
5659
|
+
Usually if the operation is an update, you may need to show an error message to the user. In such cases, you can add a side effect with useEffect, upon error.
|
|
5660
|
+
|
|
5661
|
+
First import \`useEffect\` to your component.
|
|
5662
|
+
${"```tsx"}
|
|
5663
|
+
import { useEffect } from 'react'
|
|
5664
|
+
${"```"}
|
|
5665
|
+
|
|
5666
|
+
Then add the side effect.
|
|
5667
|
+
${"```tsx"}
|
|
5668
|
+
useEffect(() => {
|
|
5669
|
+
if (isError(${camelCase(descriptor.name)}Resp)) {
|
|
5670
|
+
//TODO execute error related operation.
|
|
5671
|
+
}
|
|
5672
|
+
}, [${camelCase(descriptor.name)}Resp])
|
|
5673
|
+
${"```"}
|
|
5674
|
+
</details>
|
|
5675
|
+
|
|
5676
|
+
### Replications
|
|
5677
|
+
|
|
5678
|
+
<details>
|
|
5679
|
+
<summary>Multiple instances for same hook.</summary>
|
|
5680
|
+
|
|
5681
|
+
In some case you may need to instanciate multiple instances of the same request. For an example comparision of the products.
|
|
5682
|
+
In that cases, you can use \`key\` property to distinguish between these Network states.
|
|
5683
|
+
|
|
5684
|
+
${'```tsx'}
|
|
5685
|
+
const [${camelCase(descriptor.name)}Resp, ${camelCase(descriptor.name)}, clear${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}({ key: 'id1' });
|
|
5686
|
+
${'```'}
|
|
5687
|
+
</details>
|
|
5688
|
+
|
|
5689
|
+
|
|
5690
|
+
`;
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
;// ../../lib/next-binding/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts
|
|
5694
|
+
|
|
5695
|
+
|
|
5696
|
+
function extractAsyncHookShape(response, requestBody, imports) {
|
|
5697
|
+
if (response) {
|
|
5698
|
+
if (requestBody) {
|
|
5699
|
+
imports.add(`import { BinaryFunctionAsyncHook } from "@intrig/next"`);
|
|
5700
|
+
return `BinaryFunctionAsyncHook<Params, RequestBody, Response, _ErrorType>`;
|
|
5701
|
+
} else {
|
|
5702
|
+
imports.add(`import { UnaryFunctionAsyncHook } from "@intrig/next"`);
|
|
5703
|
+
return `UnaryFunctionAsyncHook<Params, Response, _ErrorType>`;
|
|
5704
|
+
}
|
|
5705
|
+
} else {
|
|
5706
|
+
if (requestBody) {
|
|
5707
|
+
imports.add(`import { BinaryProduceAsyncHook } from "@intrig/next"`);
|
|
5708
|
+
return `BinaryProduceAsyncHook<Params, RequestBody, _ErrorType>`;
|
|
5709
|
+
} else {
|
|
5710
|
+
imports.add(`import { UnaryProduceAsyncHook } from "@intrig/next"`);
|
|
5711
|
+
return `UnaryProduceAsyncHook<Params, _ErrorType>`;
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5715
|
+
function asyncFunctionHook_template_extractErrorParams(errorTypes) {
|
|
5716
|
+
switch(errorTypes.length){
|
|
5717
|
+
case 0:
|
|
5718
|
+
return `
|
|
5719
|
+
export type _ErrorType = any
|
|
5720
|
+
const errorSchema = z.any()`;
|
|
5721
|
+
case 1:
|
|
5722
|
+
return `
|
|
5723
|
+
export type _ErrorType = ${errorTypes[0]}
|
|
5724
|
+
const errorSchema = ${errorTypes[0]}Schema`;
|
|
5725
|
+
default:
|
|
5726
|
+
return `
|
|
5727
|
+
export type _ErrorType = ${errorTypes.join(' | ')}
|
|
5728
|
+
const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
function asyncFunctionHook_template_extractParamDeconstruction(variables, requestBody) {
|
|
5732
|
+
const isParamMandatory = variables?.some((a)=>a.in === 'path') || false;
|
|
5733
|
+
if (requestBody) {
|
|
5734
|
+
if (isParamMandatory) {
|
|
5735
|
+
return {
|
|
5736
|
+
paramExpression: 'data, p',
|
|
5737
|
+
paramType: 'data: RequestBody, params: Params'
|
|
5738
|
+
};
|
|
5739
|
+
} else {
|
|
5740
|
+
return {
|
|
5741
|
+
paramExpression: 'data, p = {}',
|
|
5742
|
+
paramType: 'data: RequestBody, params?: Params'
|
|
5743
|
+
};
|
|
5744
|
+
}
|
|
5745
|
+
} else {
|
|
5746
|
+
if (isParamMandatory) {
|
|
5747
|
+
return {
|
|
5748
|
+
paramExpression: 'p',
|
|
5749
|
+
paramType: 'params: Params'
|
|
5750
|
+
};
|
|
5751
|
+
} else {
|
|
5752
|
+
return {
|
|
5753
|
+
paramExpression: 'p = {}',
|
|
5754
|
+
paramType: 'params?: Params'
|
|
5755
|
+
};
|
|
5756
|
+
}
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
async function nextAsyncFunctionHookTemplate({ source, data: { paths, operationId, response, requestUrl, variables, requestBody, contentType, responseType, errorResponses, method } }, _path, ctx) {
|
|
5760
|
+
const postfix = ctx.potentiallyConflictingDescriptors.includes(operationId) ? generatePostfix(contentType, responseType) : '';
|
|
5761
|
+
const ts = typescript(external_path_namespaceObject.resolve(_path, 'src', source, ...paths, camelCase(operationId), `use${pascalCase(operationId)}Async${postfix}.ts`));
|
|
5762
|
+
const modifiedRequestUrl = `${requestUrl?.replace(/\{/g, "${")}`;
|
|
5763
|
+
const imports = new Set();
|
|
5764
|
+
// Basic imports
|
|
5765
|
+
imports.add(`import { z } from 'zod'`);
|
|
5766
|
+
imports.add(`import { useCallback } from 'react'`);
|
|
5767
|
+
imports.add(`import { useTransientCall, encode, isError, isSuccess } from '@intrig/next'`);
|
|
5768
|
+
// Hook signature type
|
|
5769
|
+
const hookShape = extractAsyncHookShape(response, requestBody, imports);
|
|
5770
|
+
// Add body/response param imports
|
|
5771
|
+
if (requestBody) {
|
|
5772
|
+
imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/next/${source}/components/schemas/${requestBody}"`);
|
|
5773
|
+
}
|
|
5774
|
+
if (response) {
|
|
5775
|
+
imports.add(`import { ${response} as Response, ${response}Schema as schema } from "@intrig/next/${source}/components/schemas/${response}"`);
|
|
5776
|
+
}
|
|
5777
|
+
imports.add(`import { ${pascalCase(operationId)}Params as Params } from './${pascalCase(operationId)}.params'`);
|
|
5778
|
+
// Error types
|
|
5779
|
+
const errorTypes = [
|
|
5780
|
+
...new Set(Object.values(errorResponses ?? {}).map((a)=>a.response))
|
|
5781
|
+
];
|
|
5782
|
+
errorTypes.forEach((ref)=>imports.add(`import { ${ref}, ${ref}Schema } from "@intrig/next/${source}/components/schemas/${ref}"`));
|
|
5783
|
+
// Error schema block
|
|
5784
|
+
const errorSchemaBlock = asyncFunctionHook_template_extractErrorParams(errorTypes.map((a)=>a));
|
|
5785
|
+
// Param deconstruction
|
|
5786
|
+
const { paramExpression, paramType } = asyncFunctionHook_template_extractParamDeconstruction(variables ?? [], requestBody);
|
|
5787
|
+
const paramExplode = [
|
|
5788
|
+
...variables?.filter((a)=>a.in === 'path').map((a)=>a.name) ?? [],
|
|
5789
|
+
'...params'
|
|
5790
|
+
].join(',');
|
|
5791
|
+
const finalRequestBodyBlock = requestBody ? `, data: encode(data, "${contentType}", requestBodySchema)` : '';
|
|
5792
|
+
function responseTypePart() {
|
|
5793
|
+
switch(responseType){
|
|
5794
|
+
case "application/octet-stream":
|
|
5795
|
+
return `responseType: 'blob', adapter: 'fetch',`;
|
|
5796
|
+
case "text/event-stream":
|
|
5797
|
+
return `responseType: 'stream', adapter: 'fetch',`;
|
|
5798
|
+
}
|
|
5799
|
+
return '';
|
|
5800
|
+
}
|
|
5801
|
+
return ts`
|
|
5802
|
+
${[
|
|
5803
|
+
...imports
|
|
5804
|
+
].join('\n')}
|
|
5805
|
+
|
|
5806
|
+
${!response ? `
|
|
5807
|
+
type Response = any;
|
|
5808
|
+
const schema = z.any();
|
|
5809
|
+
` : ''}
|
|
5810
|
+
|
|
5811
|
+
${errorSchemaBlock}
|
|
5812
|
+
|
|
5813
|
+
const operation = "${method.toUpperCase()} ${requestUrl}| ${contentType} -> ${responseType}";
|
|
5814
|
+
const source = "${source}";
|
|
5815
|
+
|
|
5816
|
+
function use${pascalCase(operationId)}AsyncHook(): [(${paramType}) => Promise<Response>, () => void] {
|
|
5817
|
+
const [call, abort] = useTransientCall<Response, _ErrorType>({
|
|
5818
|
+
schema,
|
|
5819
|
+
errorSchema
|
|
5820
|
+
});
|
|
5821
|
+
|
|
5822
|
+
const doExecute = useCallback<(${paramType}) => Promise<Response>>(async (${paramExpression}) => {
|
|
5823
|
+
let { ${paramExplode} } = p;
|
|
5824
|
+
|
|
5825
|
+
${requestBody ? `
|
|
5826
|
+
const validationResult = requestBodySchema.safeParse(data);
|
|
5827
|
+
if (!validationResult.success) {
|
|
5828
|
+
return Promise.reject(validationResult.error);
|
|
5829
|
+
}
|
|
5830
|
+
` : ''}
|
|
5831
|
+
|
|
5832
|
+
return await call({
|
|
5833
|
+
method: '${method}',
|
|
5834
|
+
url: \`${modifiedRequestUrl}\`,
|
|
5835
|
+
headers: {
|
|
5836
|
+
${contentType ? `"Content-Type": "${contentType}",` : ''}
|
|
5837
|
+
},
|
|
5838
|
+
params,
|
|
5839
|
+
key: \`${"${source}: ${operation}"}\`,
|
|
5840
|
+
source: '${source}'
|
|
5841
|
+
${requestBody ? finalRequestBodyBlock : ''},
|
|
5842
|
+
${responseTypePart()}
|
|
5843
|
+
});
|
|
5844
|
+
}, [call]);
|
|
5845
|
+
|
|
5846
|
+
return [doExecute, abort];
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
use${pascalCase(operationId)}AsyncHook.key = \`${"${source}: ${operation}"}\`;
|
|
5850
|
+
|
|
5851
|
+
export const use${pascalCase(operationId)}Async: ${hookShape} = use${pascalCase(operationId)}AsyncHook;
|
|
5852
|
+
`;
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
;// ../../lib/next-binding/src/lib/templates/type-utils.template.ts
|
|
5856
|
+
|
|
5857
|
+
|
|
5858
|
+
function typeUtilsTemplate(_path) {
|
|
5859
|
+
const ts = typescript(external_path_default().resolve(_path, 'src', 'type-utils.ts'));
|
|
5860
|
+
return ts`import { z } from 'zod';
|
|
5861
|
+
|
|
5862
|
+
export type BinaryData = Blob;
|
|
5863
|
+
export const BinaryDataSchema: z.ZodType<BinaryData> = z.instanceof(Blob);
|
|
5864
|
+
|
|
5865
|
+
// Base64 helpers (browser + Node compatible; no Buffer required)
|
|
5866
|
+
export function base64ToUint8Array(b64: string): Uint8Array {
|
|
5867
|
+
if (typeof atob === 'function') {
|
|
5868
|
+
// Browser
|
|
5869
|
+
const bin = atob(b64);
|
|
5870
|
+
const bytes = new Uint8Array(bin.length);
|
|
5871
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
5872
|
+
return bytes;
|
|
5873
|
+
} else {
|
|
5874
|
+
// Node
|
|
5875
|
+
const buf = Buffer.from(b64, 'base64');
|
|
5876
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
`;
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
;// ../../lib/next-binding/src/lib/templates/docs/sse-hook.ts
|
|
5884
|
+
|
|
5885
|
+
function nextSseHookDocs(descriptor) {
|
|
5886
|
+
const md = mdLiteral('sse-hook.md');
|
|
5887
|
+
const requestBody = descriptor.data.requestBody ? camelCase(descriptor.data.requestBody) : undefined;
|
|
5888
|
+
const params = descriptor.data.variables?.filter((a)=>a.in.toUpperCase() === 'PATH')?.length ? 'params' : undefined;
|
|
5889
|
+
return md`
|
|
5890
|
+
> Intrig-generated hooks are intended for backend data integration. They follow the same tuple-based API pattern as React's built-in state hooks (e.g. useState).
|
|
5891
|
+
|
|
5892
|
+
## Imports
|
|
5893
|
+
|
|
5894
|
+
#### Import the hook to the component.
|
|
5895
|
+
${"```ts"}
|
|
5896
|
+
import { use${pascalCase(descriptor.name)} } from '@intrig/next/${descriptor.path}/client';
|
|
5897
|
+
${"```"}
|
|
5898
|
+
|
|
5899
|
+
#### Import the utility methods.
|
|
5900
|
+
${"```ts"}
|
|
5901
|
+
import { isSuccess } from '@intrig/next';
|
|
5902
|
+
${"```"}
|
|
5903
|
+
|
|
5904
|
+
#### Use hook inside the component.
|
|
5905
|
+
|
|
5906
|
+
${"```tsx"}
|
|
5907
|
+
const [${camelCase(descriptor.name)}Resp, ${camelCase(descriptor.name)}, clear${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}();
|
|
5908
|
+
${"```"}
|
|
5909
|
+
|
|
5910
|
+
### Usage
|
|
5911
|
+
|
|
5912
|
+
#### Execute data fetching.
|
|
5913
|
+
|
|
5914
|
+
${"```tsx"}
|
|
5915
|
+
useEffect(() => {
|
|
5916
|
+
${camelCase(descriptor.name)}(${[
|
|
5917
|
+
requestBody,
|
|
5918
|
+
params ?? '{}'
|
|
5919
|
+
].filter(Boolean).join(', ')});
|
|
5920
|
+
}, [${[
|
|
5921
|
+
requestBody,
|
|
5922
|
+
params
|
|
5923
|
+
].filter(Boolean).join(', ')}/* Dependencies */])
|
|
5924
|
+
${"```"}
|
|
5925
|
+
|
|
5926
|
+
#### Extract data from the response.
|
|
5927
|
+
\`SSE\` hooks are a special kind of hook that delivers intermediate data updates as they are received.
|
|
5928
|
+
|
|
5929
|
+
##### Minimal example.
|
|
5930
|
+
${"```tsx"}
|
|
5931
|
+
const ${camelCase(descriptor.name)} = isPending(${camelCase(descriptor.name)}Resp) ? ${camelCase(descriptor.name)}Resp.data : null;
|
|
5932
|
+
${"```"}
|
|
5933
|
+
|
|
5934
|
+
### Full example.
|
|
5935
|
+
|
|
5936
|
+
#### Short format.
|
|
5937
|
+
You can pass the request body and params as props to the hook for initial data binding. This will create the data to be tightly coupled
|
|
5938
|
+
with the component lifecycle.
|
|
5939
|
+
|
|
5940
|
+
> Usually the SSE-hooks are combined with a collector mechanism to get the complete message.
|
|
5941
|
+
|
|
5942
|
+
<details>
|
|
5943
|
+
<summary>Implementation</summary>
|
|
5944
|
+
|
|
5945
|
+
${"```tsx"}
|
|
5946
|
+
import { use${pascalCase(descriptor.name)} } from '@intrig/next/${descriptor.path}/client';
|
|
5947
|
+
import { isSuccess, isPending, isError } from '@intrig/next';
|
|
5948
|
+
${requestBody ? `import { ${pascalCase(requestBody)} } from '@intrig/next/${descriptor.source}/components/schemas/${pascalCase(requestBody)}';` : ''}
|
|
5949
|
+
${params ? `import { ${pascalCase(params)}Params } from '@intrig/next/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
|
|
5950
|
+
import { useState, useEffect } from 'react';
|
|
5951
|
+
import { LoadingIndicator, ErrorDisplay } from '@/components/ui'; //import from your project.
|
|
5952
|
+
import {flushSync} from "react-dom";
|
|
5953
|
+
|
|
5954
|
+
${requestBody || params ? `
|
|
5955
|
+
interface MyComponentProps {
|
|
5956
|
+
${requestBody ? `${camelCase(requestBody)}: ${pascalCase(requestBody)};` : ''}
|
|
5957
|
+
${params ? `${camelCase(params)}Params: ${pascalCase(params)}Params;` : ''}
|
|
5958
|
+
}
|
|
5959
|
+
` : ''}
|
|
5960
|
+
|
|
5961
|
+
function MyComponent(${requestBody || params ? 'props: MyComponentProps' : ''}) {
|
|
5962
|
+
const [${camelCase(descriptor.name)}Resp] = use${pascalCase(descriptor.name)}({
|
|
5963
|
+
fetchOnMount: true,
|
|
5964
|
+
clearOnUnmount: true,
|
|
5965
|
+
${requestBody ? `body: props.${camelCase(requestBody)},` : ''}
|
|
5966
|
+
${params ? `params: props.${camelCase(params)},` : 'params: {}'}
|
|
5967
|
+
});
|
|
5968
|
+
|
|
5969
|
+
let [data, setData] = useState<${pascalCase(descriptor.name)}[]>([]);
|
|
5970
|
+
|
|
5971
|
+
useEffect(() => {
|
|
5972
|
+
if (isPending(${camelCase(descriptor.name)}Resp)) {
|
|
5973
|
+
flushSync(() => { //Sometimes react tends to skip intermediate renders for the performance. Use flushSync if you need to keep track of messages.
|
|
5974
|
+
setData(data => [...data, ${camelCase(descriptor.name)}Resp.data]);
|
|
5975
|
+
})
|
|
5976
|
+
}
|
|
5977
|
+
}, [${camelCase(descriptor.name)}Resp])
|
|
5978
|
+
|
|
5979
|
+
if (isPending(${camelCase(descriptor.name)}Resp)) {
|
|
5980
|
+
return <>{data.map(a => <>{JSON.stringify(a)}</>)}</>
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5983
|
+
if (isError(${camelCase(descriptor.name)}Resp)) {
|
|
5984
|
+
return <ErrorDisplay error={${camelCase(descriptor.name)}Resp.error}/> //TODO add your error view here.
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
return <>
|
|
5988
|
+
Completed: {JSON.stringify(data)}
|
|
5989
|
+
</>
|
|
5990
|
+
}
|
|
5991
|
+
${"```"}
|
|
5992
|
+
</details>
|
|
5993
|
+
|
|
5994
|
+
#### Controlled format.
|
|
5995
|
+
If you need more control over the data binding, where the data control is outside of the component lifecycle,
|
|
5996
|
+
you can use the actions provided by the hook.
|
|
5997
|
+
|
|
5998
|
+
<details>
|
|
5999
|
+
<summary>Implementation</summary>
|
|
6000
|
+
|
|
6001
|
+
${"```tsx"}
|
|
6002
|
+
import { use${pascalCase(descriptor.name)} } from '@intrig/next/${descriptor.path}/client';
|
|
6003
|
+
import { isSuccess } from '@intrig/next';
|
|
6004
|
+
${requestBody ? `import { ${pascalCase(requestBody)} } from '@intrig/next/${descriptor.source}/components/schemas/${pascalCase(requestBody)}';` : ''}
|
|
6005
|
+
${params ? `import { ${pascalCase(params)}Params } from '@intrig/next/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
|
|
6006
|
+
import {flushSync} from "react-dom";
|
|
6007
|
+
|
|
6008
|
+
${requestBody || params ? `
|
|
6009
|
+
interface MyComponentProps {
|
|
6010
|
+
${requestBody ? `${camelCase(requestBody)}: ${pascalCase(requestBody)};` : ''}
|
|
6011
|
+
${params ? `${camelCase(params)}Params: ${pascalCase(params)}Params;` : ''}
|
|
6012
|
+
}
|
|
6013
|
+
` : ''}
|
|
6014
|
+
|
|
6015
|
+
function MyComponent() {
|
|
6016
|
+
const [${camelCase(descriptor.name)}Resp, ${camelCase(descriptor.name)}, clear${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}();
|
|
6017
|
+
|
|
6018
|
+
useEffect(() => {
|
|
6019
|
+
${camelCase(descriptor.name)}(${requestBody ? `${camelCase(requestBody)},` : ''} ${params ? `${camelCase(params)},` : '{}'}) //Call the fetch function.
|
|
6020
|
+
return clear${pascalCase(descriptor.name)}; //Clear the data on unmount.
|
|
6021
|
+
}, [])
|
|
6022
|
+
|
|
6023
|
+
let [data, setData] = useState<${pascalCase(descriptor.name)}[]>([]);
|
|
6024
|
+
|
|
6025
|
+
useEffect(() => {
|
|
6026
|
+
if (isPending(${camelCase(descriptor.name)}Resp)) {
|
|
6027
|
+
flushSync(() => { //Sometimes react tends to skip intermediate renders for the performance. Use flushSync if you need to keep track of messages.
|
|
6028
|
+
setData(data => [...data, ${camelCase(descriptor.name)}Resp.data]);
|
|
6029
|
+
})
|
|
6030
|
+
}
|
|
6031
|
+
}, [${camelCase(descriptor.name)}Resp])
|
|
6032
|
+
|
|
6033
|
+
if (isPending(${camelCase(descriptor.name)}Resp)) {
|
|
6034
|
+
return <>{data.map(a => <>{JSON.stringify(a)}</>)}</>
|
|
6035
|
+
}
|
|
6036
|
+
|
|
6037
|
+
if (isError(${camelCase(descriptor.name)}Resp)) {
|
|
6038
|
+
return <ErrorDisplay error={${camelCase(descriptor.name)}Resp.error}/> //TODO add your error view here.
|
|
6039
|
+
}
|
|
6040
|
+
|
|
6041
|
+
return <>
|
|
6042
|
+
Completed: {JSON.stringify(data)}
|
|
6043
|
+
</>
|
|
6044
|
+
}
|
|
6045
|
+
${"```"}
|
|
6046
|
+
</details>
|
|
6047
|
+
|
|
6048
|
+
`;
|
|
6049
|
+
}
|
|
5546
6050
|
|
|
5547
|
-
|
|
5548
|
-
The error state extraction is done using the \`isError\` utility method. Make sure to import the method into your component.
|
|
6051
|
+
;// ../../lib/next-binding/src/lib/templates/docs/async-hook.ts
|
|
5549
6052
|
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
6053
|
+
function nextAsyncFunctionHookDocs(descriptor) {
|
|
6054
|
+
const md = mdLiteral('async-hook.md');
|
|
6055
|
+
const requestBody = descriptor.data.requestBody ? camelCase(descriptor.data.requestBody) : undefined;
|
|
6056
|
+
const params = descriptor.data.variables?.filter((a)=>a.in.toUpperCase() === 'PATH')?.length ? 'params' : undefined;
|
|
6057
|
+
return md`
|
|
6058
|
+
|
|
6059
|
+
> Intrig generated async hooks are intended to use for the rapid usecases like validations. This effectively bypasses the network-state management.
|
|
6060
|
+
> The async hooks follow the tuple-based API pattern as React's built-in state hooks (e.g. useState).
|
|
5553
6061
|
|
|
5554
|
-
|
|
5555
|
-
<summary>Early return</summary>
|
|
5556
|
-
In most cases, in an error state, you may want to show the error message to the user instead of real component. In that case, you can add a conditional early return.
|
|
6062
|
+
## Imports
|
|
5557
6063
|
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
}
|
|
6064
|
+
#### Import the hook to the component.
|
|
6065
|
+
${"```ts"}
|
|
6066
|
+
import { use${pascalCase(descriptor.name)}Async } from '@intrig/next/${descriptor.path}/client';
|
|
5562
6067
|
${"```"}
|
|
5563
|
-
</details>
|
|
5564
6068
|
|
|
5565
|
-
|
|
5566
|
-
<summary>Inline rendering</summary>
|
|
5567
|
-
If you need to show a non-blocking/parallel error indicator, or change the component behavior based on the error state, you can inline the error condition.
|
|
6069
|
+
#### Use hook inside the component.
|
|
5568
6070
|
|
|
5569
6071
|
${"```tsx"}
|
|
5570
|
-
|
|
6072
|
+
const [${camelCase(descriptor.name)}, abort${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}Async();
|
|
5571
6073
|
${"```"}
|
|
5572
|
-
</details>
|
|
5573
6074
|
|
|
5574
|
-
|
|
5575
|
-
<summary>Side effect upon error</summary>
|
|
5576
|
-
|
|
5577
|
-
Usually if the operation is an update, you may need to show an error message to the user. In such cases, you can add a side effect with useEffect, upon error.
|
|
6075
|
+
#### Execute data fetching / calling.
|
|
5578
6076
|
|
|
5579
|
-
First import \`useEffect\` to your component.
|
|
5580
6077
|
${"```tsx"}
|
|
5581
|
-
|
|
5582
|
-
${
|
|
6078
|
+
const fn = useCallback(async () => {
|
|
6079
|
+
let ${camelCase(descriptor.name)}Data = await ${camelCase(descriptor.name)}(${[
|
|
6080
|
+
requestBody,
|
|
6081
|
+
params ?? '{}'
|
|
6082
|
+
].filter(Boolean).join(', ')});
|
|
6083
|
+
//TODO do something with the ${camelCase(descriptor.name)}Data.
|
|
6084
|
+
return ${camelCase(descriptor.name)}Data;
|
|
6085
|
+
}, [${[
|
|
6086
|
+
requestBody,
|
|
6087
|
+
params
|
|
6088
|
+
].filter(Boolean).join(', ')}/* Dependencies */])
|
|
5583
6089
|
|
|
5584
|
-
Then add the side effect.
|
|
5585
|
-
${"```tsx"}
|
|
5586
|
-
useEffect(() => {
|
|
5587
|
-
if (isError(${camelCase(descriptor.name)}Resp)) {
|
|
5588
|
-
//TODO execute error related operation.
|
|
5589
|
-
}
|
|
5590
|
-
}, [${camelCase(descriptor.name)}Resp])
|
|
5591
6090
|
${"```"}
|
|
5592
|
-
</details>
|
|
5593
6091
|
|
|
5594
|
-
|
|
6092
|
+
## Full example
|
|
5595
6093
|
|
|
5596
6094
|
<details>
|
|
5597
|
-
<summary>
|
|
6095
|
+
<summary>Implementation</summary>
|
|
5598
6096
|
|
|
5599
|
-
|
|
5600
|
-
|
|
6097
|
+
${"```tsx"}
|
|
6098
|
+
import { use${pascalCase(descriptor.name)}Async } from '@intrig/next/${descriptor.path}/client';
|
|
6099
|
+
${requestBody ? `import { ${pascalCase(requestBody)} } from '@intrig/next/${descriptor.source}/components/schemas/${pascalCase(requestBody)}';` : ''}
|
|
6100
|
+
${params ? `import { ${pascalCase(params)}Params } from '@intrig/next/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
|
|
6101
|
+
import { useCallback } from 'react';
|
|
5601
6102
|
|
|
5602
|
-
${
|
|
5603
|
-
|
|
5604
|
-
${'
|
|
5605
|
-
|
|
6103
|
+
${requestBody || params ? `
|
|
6104
|
+
interface MyComponentProps {
|
|
6105
|
+
${requestBody ? `${camelCase(requestBody)}: ${pascalCase(requestBody)};` : ''}
|
|
6106
|
+
${params ? `${camelCase(params)}Params: ${pascalCase(params)}Params;` : ''}
|
|
6107
|
+
}
|
|
6108
|
+
` : ''}
|
|
5606
6109
|
|
|
6110
|
+
function MyComponent(${requestBody || params ? 'props: MyComponentProps' : ''}) {
|
|
6111
|
+
const [${camelCase(descriptor.name)}, abort${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}Async();
|
|
5607
6112
|
|
|
5608
|
-
|
|
6113
|
+
const fn = useCallback(async (${[
|
|
6114
|
+
requestBody,
|
|
6115
|
+
params
|
|
6116
|
+
].filter(Boolean).join(', ')}) => {
|
|
6117
|
+
return await ${camelCase(descriptor.name)}(${[
|
|
6118
|
+
requestBody,
|
|
6119
|
+
params
|
|
6120
|
+
].filter(Boolean).join(', ')});
|
|
6121
|
+
}, [/* Dependencies */])
|
|
6122
|
+
|
|
6123
|
+
//use fn where remote callback is needed.
|
|
6124
|
+
return <>
|
|
6125
|
+
<button onClick={() => fn(${[
|
|
6126
|
+
requestBody,
|
|
6127
|
+
params
|
|
6128
|
+
].filter(Boolean).map((a)=>`props.${a}`).join(', ')})}>Call remote</button>
|
|
6129
|
+
</>
|
|
6130
|
+
}
|
|
6131
|
+
|
|
6132
|
+
${"```"}
|
|
6133
|
+
</details>
|
|
6134
|
+
|
|
6135
|
+
<hint>
|
|
6136
|
+
//TODO improve with usecases
|
|
6137
|
+
</hint>
|
|
6138
|
+
|
|
6139
|
+
`;
|
|
5609
6140
|
}
|
|
5610
6141
|
|
|
5611
6142
|
;// ../../lib/next-binding/src/lib/next-binding.service.ts
|
|
@@ -5641,6 +6172,10 @@ function next_binding_service_ts_metadata(k, v) {
|
|
|
5641
6172
|
|
|
5642
6173
|
|
|
5643
6174
|
|
|
6175
|
+
|
|
6176
|
+
|
|
6177
|
+
|
|
6178
|
+
|
|
5644
6179
|
|
|
5645
6180
|
|
|
5646
6181
|
|
|
@@ -5650,11 +6185,12 @@ const nonDownloadMimePatterns = external_picomatch_default()([
|
|
|
5650
6185
|
"application/json",
|
|
5651
6186
|
"application/xml",
|
|
5652
6187
|
"application/x-www-form-urlencoded",
|
|
6188
|
+
"application/event-stream",
|
|
5653
6189
|
"text/*"
|
|
5654
6190
|
]);
|
|
5655
6191
|
class IntrigNextBindingService extends GeneratorBinding {
|
|
5656
6192
|
constructor(sourceManagementService, config){
|
|
5657
|
-
super(), this.sourceManagementService = sourceManagementService, this.config = config, this.dump = this.sourceManagementService.dump, this._path = this.config.get("generatedDir") ?? external_path_default().resolve(external_node_process_namespaceObject.cwd(), '.intrig', 'generated');
|
|
6193
|
+
super(), this.sourceManagementService = sourceManagementService, this.config = config, this.logger = new common_namespaceObject.Logger(IntrigNextBindingService.name), this.dump = this.sourceManagementService.dump, this._path = this.config.get("generatedDir") ?? external_path_default().resolve(external_node_process_namespaceObject.cwd(), '.intrig', 'generated');
|
|
5658
6194
|
}
|
|
5659
6195
|
getRestOptions() {
|
|
5660
6196
|
return {
|
|
@@ -5686,12 +6222,19 @@ class IntrigNextBindingService extends GeneratorBinding {
|
|
|
5686
6222
|
await this.dump(nextExtraTemplate(this._path));
|
|
5687
6223
|
await this.dump(nextLoggerTemplate(this._path));
|
|
5688
6224
|
await this.dump(nextSwcrcTemplate(this._path));
|
|
6225
|
+
await this.dump(typeUtilsTemplate(this._path));
|
|
5689
6226
|
}
|
|
5690
6227
|
async generateSource(descriptors, source) {
|
|
6228
|
+
//TODO improve this logic to catch potential conflicts.
|
|
6229
|
+
const potentiallyConflictingDescriptors = descriptors.filter(isRestDescriptor).sort((a, b)=>(a.data.contentType === "application/json" ? -1 : 0) - (b.data.contentType === "application/json" ? -1 : 0)).filter((descriptor, index, array)=>array.findIndex((other)=>other.data.operationId === descriptor.data.operationId) !== index).map((descriptor)=>descriptor.id);
|
|
6230
|
+
const ctx = {
|
|
6231
|
+
potentiallyConflictingDescriptors
|
|
6232
|
+
};
|
|
5691
6233
|
const groupedByPath = {};
|
|
5692
6234
|
for (const descriptor of descriptors){
|
|
6235
|
+
this.logger.log(`Generating source: ${descriptor.name}`);
|
|
5693
6236
|
if (isRestDescriptor(descriptor)) {
|
|
5694
|
-
await this.generateRestSource(source, descriptor);
|
|
6237
|
+
await this.generateRestSource(source, descriptor, ctx);
|
|
5695
6238
|
groupedByPath[descriptor.data.requestUrl] = groupedByPath[descriptor.data.requestUrl] || [];
|
|
5696
6239
|
groupedByPath[descriptor.data.requestUrl].push(descriptor);
|
|
5697
6240
|
} else if (isSchemaDescriptor(descriptor)) {
|
|
@@ -5702,14 +6245,14 @@ class IntrigNextBindingService extends GeneratorBinding {
|
|
|
5702
6245
|
await this.dump(nextRequestRouteTemplate(requestUrl, matchingPaths, this._path));
|
|
5703
6246
|
}
|
|
5704
6247
|
}
|
|
5705
|
-
async generateRestSource(source, descriptor) {
|
|
6248
|
+
async generateRestSource(source, descriptor, ctx) {
|
|
5706
6249
|
const clientExports = [];
|
|
5707
6250
|
const serverExports = [];
|
|
5708
6251
|
await this.dump(nextParamsTemplate(descriptor, clientExports, serverExports, this._path));
|
|
5709
6252
|
await this.dump(nextRequestHookTemplate(descriptor, clientExports, serverExports, this._path));
|
|
5710
6253
|
await this.dump(nextRequestMethodTemplate(descriptor, clientExports, serverExports, this._path));
|
|
5711
|
-
|
|
5712
|
-
if (descriptor.data.method.toUpperCase() === 'GET' && !nonDownloadMimePatterns(descriptor.data.responseType)) {
|
|
6254
|
+
await this.dump(nextAsyncFunctionHookTemplate(descriptor, this._path, ctx));
|
|
6255
|
+
if (descriptor.data.method.toUpperCase() === 'GET' && (!nonDownloadMimePatterns(descriptor.data.responseType) || descriptor.data.responseType !== '*/*') || descriptor.data.responseHeaders?.['content-disposition']) {
|
|
5713
6256
|
await this.dump(nextDownloadHookTemplate(descriptor, clientExports, serverExports, this._path));
|
|
5714
6257
|
}
|
|
5715
6258
|
await this.dump(nextClientIndexTemplate([
|
|
@@ -5796,16 +6339,20 @@ ${"```"}
|
|
|
5796
6339
|
]));
|
|
5797
6340
|
const tabs = [];
|
|
5798
6341
|
if (result.data.responseType === 'text/event-stream') {
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
6342
|
+
tabs.push({
|
|
6343
|
+
name: 'SSE Hook',
|
|
6344
|
+
content: (await nextSseHookDocs(result)).content
|
|
6345
|
+
});
|
|
5803
6346
|
} else {
|
|
5804
6347
|
tabs.push({
|
|
5805
6348
|
name: 'React Hook',
|
|
5806
6349
|
content: (await nextReactHookDocs(result)).content
|
|
5807
6350
|
});
|
|
5808
6351
|
}
|
|
6352
|
+
tabs.push({
|
|
6353
|
+
name: 'Async Hook',
|
|
6354
|
+
content: (await nextAsyncFunctionHookDocs(result)).content
|
|
6355
|
+
});
|
|
5809
6356
|
return RestDocumentation.from({
|
|
5810
6357
|
id: result.id,
|
|
5811
6358
|
name: result.name,
|
|
@@ -7114,6 +7661,7 @@ function reactPackageJsonTemplate(_path) {
|
|
|
7114
7661
|
"@swc/cli": "^0.7.7",
|
|
7115
7662
|
"@swc/core": "^1.12.6",
|
|
7116
7663
|
"@types/node": "^24.0.4",
|
|
7664
|
+
"typescript": "${packageJson.devDependencies.typescript ?? packageJson.dependencies.typescript}",
|
|
7117
7665
|
"react": "${packageJson.dependencies.react}",
|
|
7118
7666
|
"react-dom": "${packageJson.dependencies['react-dom']}"
|
|
7119
7667
|
},
|
|
@@ -7913,6 +8461,15 @@ async function reactRequestHookTemplate({ source, data: { paths, operationId, re
|
|
|
7913
8461
|
"...params"
|
|
7914
8462
|
].join(",");
|
|
7915
8463
|
const finalRequestBodyBlock = requestBody ? `,data: encode(data, "${contentType}", requestBodySchema)` : '';
|
|
8464
|
+
function responseTypePart() {
|
|
8465
|
+
switch(responseType){
|
|
8466
|
+
case "application/octet-stream":
|
|
8467
|
+
return `responseType: 'blob', adapter: 'fetch',`;
|
|
8468
|
+
case "text/event-stream":
|
|
8469
|
+
return `responseType: 'stream', adapter: 'fetch',`;
|
|
8470
|
+
}
|
|
8471
|
+
return '';
|
|
8472
|
+
}
|
|
7916
8473
|
return ts`
|
|
7917
8474
|
${[
|
|
7918
8475
|
...imports
|
|
@@ -7957,7 +8514,7 @@ async function reactRequestHookTemplate({ source, data: { paths, operationId, re
|
|
|
7957
8514
|
key: \`${"${source}: ${operation}"}\`,
|
|
7958
8515
|
source: '${source}'
|
|
7959
8516
|
${requestBody ? finalRequestBodyBlock : ''},
|
|
7960
|
-
${
|
|
8517
|
+
${responseTypePart()}
|
|
7961
8518
|
})
|
|
7962
8519
|
return successfulDispatch();
|
|
7963
8520
|
}, [dispatch])
|
|
@@ -9077,7 +9634,7 @@ ${"```"}
|
|
|
9077
9634
|
;// ../../lib/react-binding/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts
|
|
9078
9635
|
|
|
9079
9636
|
|
|
9080
|
-
function
|
|
9637
|
+
function asyncFunctionHook_template_extractAsyncHookShape(response, requestBody, imports) {
|
|
9081
9638
|
if (response) {
|
|
9082
9639
|
if (requestBody) {
|
|
9083
9640
|
imports.add(`import { BinaryFunctionAsyncHook } from "@intrig/react"`);
|
|
@@ -9096,7 +9653,7 @@ function extractAsyncHookShape(response, requestBody, imports) {
|
|
|
9096
9653
|
}
|
|
9097
9654
|
}
|
|
9098
9655
|
}
|
|
9099
|
-
function
|
|
9656
|
+
function method_asyncFunctionHook_template_extractErrorParams(errorTypes) {
|
|
9100
9657
|
switch(errorTypes.length){
|
|
9101
9658
|
case 0:
|
|
9102
9659
|
return `
|
|
@@ -9112,7 +9669,7 @@ function asyncFunctionHook_template_extractErrorParams(errorTypes) {
|
|
|
9112
9669
|
const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
|
|
9113
9670
|
}
|
|
9114
9671
|
}
|
|
9115
|
-
function
|
|
9672
|
+
function method_asyncFunctionHook_template_extractParamDeconstruction(variables, requestBody) {
|
|
9116
9673
|
const isParamMandatory = variables?.some((a)=>a.in === 'path') || false;
|
|
9117
9674
|
if (requestBody) {
|
|
9118
9675
|
if (isParamMandatory) {
|
|
@@ -9150,7 +9707,7 @@ async function reactAsyncFunctionHookTemplate({ source, data: { paths, operation
|
|
|
9150
9707
|
imports.add(`import { useCallback } from 'react'`);
|
|
9151
9708
|
imports.add(`import { useTransientCall, encode, isError, isSuccess } from '@intrig/react'`);
|
|
9152
9709
|
// Hook signature type
|
|
9153
|
-
const hookShape =
|
|
9710
|
+
const hookShape = asyncFunctionHook_template_extractAsyncHookShape(response, requestBody, imports);
|
|
9154
9711
|
// Add body/response param imports
|
|
9155
9712
|
if (requestBody) {
|
|
9156
9713
|
imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/react/${source}/components/schemas/${requestBody}"`);
|
|
@@ -9165,14 +9722,23 @@ async function reactAsyncFunctionHookTemplate({ source, data: { paths, operation
|
|
|
9165
9722
|
];
|
|
9166
9723
|
errorTypes.forEach((ref)=>imports.add(`import { ${ref}, ${ref}Schema } from "@intrig/react/${source}/components/schemas/${ref}"`));
|
|
9167
9724
|
// Error schema block
|
|
9168
|
-
const errorSchemaBlock =
|
|
9725
|
+
const errorSchemaBlock = method_asyncFunctionHook_template_extractErrorParams(errorTypes.map((a)=>a));
|
|
9169
9726
|
// Param deconstruction
|
|
9170
|
-
const { paramExpression, paramType } =
|
|
9727
|
+
const { paramExpression, paramType } = method_asyncFunctionHook_template_extractParamDeconstruction(variables ?? [], requestBody);
|
|
9171
9728
|
const paramExplode = [
|
|
9172
9729
|
...variables?.filter((a)=>a.in === 'path').map((a)=>a.name) ?? [],
|
|
9173
9730
|
'...params'
|
|
9174
9731
|
].join(',');
|
|
9175
9732
|
const finalRequestBodyBlock = requestBody ? `, data: encode(data, "${contentType}", requestBodySchema)` : '';
|
|
9733
|
+
function responseTypePart() {
|
|
9734
|
+
switch(responseType){
|
|
9735
|
+
case "application/octet-stream":
|
|
9736
|
+
return `responseType: 'blob', adapter: 'fetch',`;
|
|
9737
|
+
case "text/event-stream":
|
|
9738
|
+
return `responseType: 'stream', adapter: 'fetch',`;
|
|
9739
|
+
}
|
|
9740
|
+
return '';
|
|
9741
|
+
}
|
|
9176
9742
|
return ts`
|
|
9177
9743
|
${[
|
|
9178
9744
|
...imports
|
|
@@ -9214,7 +9780,7 @@ function use${pascalCase(operationId)}AsyncHook(): [(${paramType}) => Promise<Re
|
|
|
9214
9780
|
key: \`${"${source}: ${operation}"}\`,
|
|
9215
9781
|
source: '${source}'
|
|
9216
9782
|
${requestBody ? finalRequestBodyBlock : ''},
|
|
9217
|
-
${
|
|
9783
|
+
${responseTypePart()}
|
|
9218
9784
|
});
|
|
9219
9785
|
}, [call]);
|
|
9220
9786
|
|
|
@@ -9321,7 +9887,7 @@ ${"```"}
|
|
|
9321
9887
|
;// ../../lib/react-binding/src/lib/templates/type-utils.template.ts
|
|
9322
9888
|
|
|
9323
9889
|
|
|
9324
|
-
function
|
|
9890
|
+
function type_utils_template_typeUtilsTemplate(_path) {
|
|
9325
9891
|
const ts = typescript(external_path_default().resolve(_path, 'src', 'type-utils.ts'));
|
|
9326
9892
|
return ts`import { z } from 'zod';
|
|
9327
9893
|
|
|
@@ -9406,7 +9972,7 @@ class ReactBindingService extends GeneratorBinding {
|
|
|
9406
9972
|
await this.dump(reactPackageJsonTemplate(this._path));
|
|
9407
9973
|
await this.dump(reactProviderTemplate(this._path, apisToSync));
|
|
9408
9974
|
await this.dump(reactTsConfigTemplate(this._path));
|
|
9409
|
-
await this.dump(
|
|
9975
|
+
await this.dump(type_utils_template_typeUtilsTemplate(this._path));
|
|
9410
9976
|
}
|
|
9411
9977
|
async generateSource(descriptors, source) {
|
|
9412
9978
|
//TODO improve this logic to catch potential conflicts.
|
|
@@ -10736,10 +11302,13 @@ function deref(spec) {
|
|
|
10736
11302
|
};
|
|
10737
11303
|
}
|
|
10738
11304
|
|
|
11305
|
+
;// external "node:crypto"
|
|
11306
|
+
const external_node_crypto_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:crypto");
|
|
10739
11307
|
;// ../../lib/openapi-source/src/lib/util/normalize.ts
|
|
10740
11308
|
|
|
10741
11309
|
|
|
10742
11310
|
|
|
11311
|
+
|
|
10743
11312
|
function generateTypeName(operationOb, postFix) {
|
|
10744
11313
|
return [
|
|
10745
11314
|
operationOb.tags?.[0],
|
|
@@ -10749,7 +11318,7 @@ function generateTypeName(operationOb, postFix) {
|
|
|
10749
11318
|
}
|
|
10750
11319
|
function normalize(spec) {
|
|
10751
11320
|
const doDeref = deref(spec);
|
|
10752
|
-
|
|
11321
|
+
const normalized = (0,external_immer_namespaceObject.produce)(spec, (draft)=>{
|
|
10753
11322
|
const paths = draft.paths;
|
|
10754
11323
|
for (const [path, pathItem] of Object.entries(paths)){
|
|
10755
11324
|
const pathItemObject = pathItem;
|
|
@@ -10854,6 +11423,9 @@ function normalize(spec) {
|
|
|
10854
11423
|
}
|
|
10855
11424
|
//TODO implement fix schema types.
|
|
10856
11425
|
});
|
|
11426
|
+
return (0,external_immer_namespaceObject.produce)(normalized, (draft)=>{
|
|
11427
|
+
draft.info['x-intrig-hash'] = (0,external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify(draft)).digest('hex');
|
|
11428
|
+
});
|
|
10857
11429
|
}
|
|
10858
11430
|
|
|
10859
11431
|
;// ../../lib/openapi-source/src/lib/util/extractRequestsFromSpec.ts
|
|
@@ -10997,26 +11569,47 @@ class IntrigOpenapiService {
|
|
|
10997
11569
|
}
|
|
10998
11570
|
async sync(config, id, ctx) {
|
|
10999
11571
|
this.logger.log('Starting OpenAPI sync process');
|
|
11572
|
+
const errors = [];
|
|
11000
11573
|
if (!id) {
|
|
11574
|
+
// Process all sources with better error isolation
|
|
11001
11575
|
for (const source of config.sources){
|
|
11002
11576
|
this.logger.log(`Processing source: ${source.id}`);
|
|
11003
11577
|
try {
|
|
11004
11578
|
await this.doSync(source, config, ctx);
|
|
11579
|
+
this.logger.log(`Successfully synced source: ${source.id}`);
|
|
11005
11580
|
} catch (e) {
|
|
11006
|
-
|
|
11581
|
+
const errorMsg = `Failed to sync source ${source.id}: ${e.message}`;
|
|
11582
|
+
this.logger.error(errorMsg, e);
|
|
11583
|
+
errors.push({
|
|
11584
|
+
sourceId: source.id,
|
|
11585
|
+
error: errorMsg
|
|
11586
|
+
});
|
|
11587
|
+
// Continue with other sources instead of failing completely
|
|
11007
11588
|
}
|
|
11008
11589
|
}
|
|
11009
11590
|
} else {
|
|
11010
11591
|
const source = config.sources.find((s)=>s.id === id);
|
|
11011
11592
|
if (!source) {
|
|
11012
|
-
|
|
11013
|
-
|
|
11593
|
+
const error = `Source ${id} not found`;
|
|
11594
|
+
this.logger.error(error);
|
|
11595
|
+
throw new Error(error);
|
|
11014
11596
|
}
|
|
11015
11597
|
this.logger.log(`Processing specific source: ${id}`);
|
|
11016
11598
|
try {
|
|
11017
11599
|
await this.doSync(source, config, ctx);
|
|
11600
|
+
this.logger.log(`Successfully synced source: ${id}`);
|
|
11018
11601
|
} catch (e) {
|
|
11019
|
-
|
|
11602
|
+
const errorMsg = `Failed to sync source ${id}: ${e.message}`;
|
|
11603
|
+
this.logger.error(errorMsg, e);
|
|
11604
|
+
throw new Error(errorMsg);
|
|
11605
|
+
}
|
|
11606
|
+
}
|
|
11607
|
+
if (errors.length > 0) {
|
|
11608
|
+
const summary = `Sync completed with ${errors.length} errors: ${errors.map((e)=>e.sourceId).join(', ')}`;
|
|
11609
|
+
this.logger.warn(summary);
|
|
11610
|
+
// If all sources failed, throw an error
|
|
11611
|
+
if (errors.length === config.sources.length) {
|
|
11612
|
+
throw new Error(`All sources failed to sync: ${errors.map((e)=>e.error).join('; ')}`);
|
|
11020
11613
|
}
|
|
11021
11614
|
}
|
|
11022
11615
|
this.logger.log('OpenAPI sync process completed');
|
|
@@ -11062,7 +11655,8 @@ class IntrigOpenapiService {
|
|
|
11062
11655
|
const restData = extractRequestsFromSpec(document);
|
|
11063
11656
|
const schemas = extractSchemas(document);
|
|
11064
11657
|
const sha1 = (str)=>external_crypto_namespaceObject.createHash('sha1').update(str).digest('hex');
|
|
11065
|
-
|
|
11658
|
+
const hash = document.info['x-intrig-hash'];
|
|
11659
|
+
const descriptors = [
|
|
11066
11660
|
...restData.map((restData)=>ResourceDescriptor.from({
|
|
11067
11661
|
id: sha1(`${id}_${restData.method}_${restData.paths.join('_')}_${restData.operationId}_${restData.contentType}_${restData.responseType}`),
|
|
11068
11662
|
name: camelCase(restData.operationId),
|
|
@@ -11080,6 +11674,17 @@ class IntrigOpenapiService {
|
|
|
11080
11674
|
data: schema
|
|
11081
11675
|
}))
|
|
11082
11676
|
];
|
|
11677
|
+
return {
|
|
11678
|
+
descriptors,
|
|
11679
|
+
hash
|
|
11680
|
+
};
|
|
11681
|
+
}
|
|
11682
|
+
async getHash(id) {
|
|
11683
|
+
const document = await this.specManagementService.read(id);
|
|
11684
|
+
if (!document) {
|
|
11685
|
+
throw new Error(`Spec ${id} not found`);
|
|
11686
|
+
}
|
|
11687
|
+
return document.info['x-intrig-hash'];
|
|
11083
11688
|
}
|
|
11084
11689
|
validate(normalized, restOptions) {
|
|
11085
11690
|
function validateForConflictingVariables() {
|
|
@@ -11670,7 +12275,7 @@ class SearchService {
|
|
|
11670
12275
|
async onModuleInit() {
|
|
11671
12276
|
try {
|
|
11672
12277
|
for (const source of this.configService.get().sources){
|
|
11673
|
-
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12278
|
+
const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
|
|
11674
12279
|
descriptors.forEach((descriptor)=>this.addDescriptor(descriptor));
|
|
11675
12280
|
}
|
|
11676
12281
|
// Update the CodeAnalyzer with all descriptors
|
|
@@ -11962,24 +12567,50 @@ class OperationsService {
|
|
|
11962
12567
|
this.config = config;
|
|
11963
12568
|
this.searchService = searchService;
|
|
11964
12569
|
this.logger = new common_namespaceObject.Logger(OperationsService.name);
|
|
12570
|
+
// Add sync coordination
|
|
12571
|
+
this.syncInProgress = new Set();
|
|
12572
|
+
this.SYNC_TIMEOUT = 300000; // 5 minutes timeout
|
|
11965
12573
|
this.generateDir = this.config.get("generatedDir") ?? external_path_namespaceObject.resolve(process.cwd(), ".intrig", "generated");
|
|
11966
12574
|
}
|
|
11967
12575
|
async getConfig(ctx) {
|
|
11968
12576
|
return this.configService.get();
|
|
11969
12577
|
}
|
|
11970
12578
|
async sync(ctx, id) {
|
|
11971
|
-
const
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
12579
|
+
const syncKey = id || 'all';
|
|
12580
|
+
// Check if sync is already in progress
|
|
12581
|
+
if (this.syncInProgress.has(syncKey)) {
|
|
12582
|
+
const error = `Sync already in progress for: ${syncKey}`;
|
|
12583
|
+
this.logger.warn(error);
|
|
12584
|
+
throw new Error(error);
|
|
12585
|
+
}
|
|
12586
|
+
// Add timeout protection
|
|
12587
|
+
const timeoutId = setTimeout(()=>{
|
|
12588
|
+
this.syncInProgress.delete(syncKey);
|
|
12589
|
+
this.logger.error(`Sync timeout for ${syncKey} after ${this.SYNC_TIMEOUT}ms`);
|
|
12590
|
+
}, this.SYNC_TIMEOUT);
|
|
12591
|
+
this.syncInProgress.add(syncKey);
|
|
12592
|
+
this.logger.log(`Starting sync operation for: ${syncKey}`);
|
|
12593
|
+
try {
|
|
12594
|
+
const config = await this.getConfig(ctx);
|
|
12595
|
+
const prevDescriptors = await this.getPreviousState(ctx, config);
|
|
12596
|
+
const restOptions = this.generatorBinding.getRestOptions();
|
|
12597
|
+
await this.openApiService.sync({
|
|
12598
|
+
...config,
|
|
12599
|
+
restOptions: {
|
|
12600
|
+
...config.restOptions,
|
|
12601
|
+
...restOptions
|
|
12602
|
+
}
|
|
12603
|
+
}, id, ctx);
|
|
12604
|
+
const newDescriptors = await this.getNewState(ctx, config);
|
|
12605
|
+
await this.indexDiff(ctx, prevDescriptors, newDescriptors);
|
|
12606
|
+
this.logger.log(`Sync operation completed successfully for: ${syncKey}`);
|
|
12607
|
+
} catch (error) {
|
|
12608
|
+
this.logger.error(`Sync operation failed for ${syncKey}: ${error.message}`, error);
|
|
12609
|
+
throw error;
|
|
12610
|
+
} finally{
|
|
12611
|
+
clearTimeout(timeoutId);
|
|
12612
|
+
this.syncInProgress.delete(syncKey);
|
|
12613
|
+
}
|
|
11983
12614
|
}
|
|
11984
12615
|
async indexDiff(ctx, prevDescriptors, newDescriptors) {
|
|
11985
12616
|
this.searchService.clearAll();
|
|
@@ -11987,21 +12618,36 @@ class OperationsService {
|
|
|
11987
12618
|
}
|
|
11988
12619
|
async getPreviousState(ctx, config) {
|
|
11989
12620
|
let prevDescriptors = [];
|
|
12621
|
+
const errors = [];
|
|
11990
12622
|
for (const source of config.sources){
|
|
11991
12623
|
try {
|
|
11992
|
-
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12624
|
+
const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
|
|
11993
12625
|
prevDescriptors = [
|
|
11994
12626
|
...prevDescriptors,
|
|
11995
12627
|
...descriptors
|
|
11996
12628
|
];
|
|
11997
|
-
|
|
12629
|
+
this.logger.debug(`Loaded ${descriptors.length} descriptors from source ${source.id}`);
|
|
12630
|
+
} catch (e) {
|
|
12631
|
+
const errorMsg = `Failed to load previous state for source ${source.id}: ${e.message}`;
|
|
12632
|
+
this.logger.warn(errorMsg);
|
|
12633
|
+
errors.push(errorMsg);
|
|
12634
|
+
// If it's a critical error (not just missing file), we might want to fail
|
|
12635
|
+
if (e.message.includes('Invalid JSON') || e.message.includes('corrupted')) {
|
|
12636
|
+
throw new Error(`Critical error in getPreviousState: ${errorMsg}`);
|
|
12637
|
+
}
|
|
12638
|
+
}
|
|
12639
|
+
}
|
|
12640
|
+
if (errors.length > 0) {
|
|
12641
|
+
this.logger.warn(`getPreviousState completed with ${errors.length} errors: ${errors.join('; ')}`);
|
|
11998
12642
|
}
|
|
12643
|
+
this.logger.log(`Loaded total of ${prevDescriptors.length} previous descriptors`);
|
|
11999
12644
|
return prevDescriptors;
|
|
12000
12645
|
}
|
|
12001
12646
|
async getNewState(ctx, config) {
|
|
12647
|
+
this.logger.log('Loading new state');
|
|
12002
12648
|
let prevDescriptors = [];
|
|
12003
12649
|
for (const source of config.sources){
|
|
12004
|
-
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12650
|
+
const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
|
|
12005
12651
|
prevDescriptors = [
|
|
12006
12652
|
...prevDescriptors,
|
|
12007
12653
|
...descriptors
|
|
@@ -12033,8 +12679,10 @@ class OperationsService {
|
|
|
12033
12679
|
async generate(ctx) {
|
|
12034
12680
|
const config = await this.getConfig(ctx);
|
|
12035
12681
|
await this.clearGenerateDir(ctx);
|
|
12682
|
+
const hashes = {};
|
|
12036
12683
|
for (const source of config.sources){
|
|
12037
|
-
const descriptors = await this.getDescriptors(ctx, source);
|
|
12684
|
+
const { descriptors, hash } = await this.getDescriptors(ctx, source);
|
|
12685
|
+
hashes[source.id] = hash;
|
|
12038
12686
|
await this.generateSourceContent(ctx, descriptors, source);
|
|
12039
12687
|
}
|
|
12040
12688
|
await this.generateGlobalContent(ctx, config.sources);
|
|
@@ -12042,9 +12690,9 @@ class OperationsService {
|
|
|
12042
12690
|
await this.buildContent(ctx);
|
|
12043
12691
|
const tempBuildContext = config.__dangorouslyOverrideBuild;
|
|
12044
12692
|
if (tempBuildContext) {
|
|
12045
|
-
this.copyContentToSource(ctx, tempBuildContext);
|
|
12693
|
+
await this.copyContentToSource(ctx, tempBuildContext);
|
|
12046
12694
|
} else {
|
|
12047
|
-
await this.copyContentToNodeModules(ctx);
|
|
12695
|
+
await this.copyContentToNodeModules(ctx, hashes);
|
|
12048
12696
|
}
|
|
12049
12697
|
await this.executePostBuild(ctx);
|
|
12050
12698
|
}
|
|
@@ -12076,7 +12724,7 @@ class OperationsService {
|
|
|
12076
12724
|
throw error;
|
|
12077
12725
|
}
|
|
12078
12726
|
}
|
|
12079
|
-
async copyContentToNodeModules(ctx) {
|
|
12727
|
+
async copyContentToNodeModules(ctx, hashes) {
|
|
12080
12728
|
const targetLibDir = external_path_namespaceObject.join(this.config.get('rootDir') ?? process.cwd(), 'node_modules', '@intrig', this.generatorBinding.getLibName());
|
|
12081
12729
|
try {
|
|
12082
12730
|
if (await external_fs_extra_namespaceObject.pathExists(external_path_namespaceObject.join(targetLibDir, 'src'))) {
|
|
@@ -12088,6 +12736,10 @@ class OperationsService {
|
|
|
12088
12736
|
this.logger.log(`Copied ${targetLibDir}`);
|
|
12089
12737
|
await this.mergePackageJson(external_path_namespaceObject.join(this.generateDir, 'package.json'), external_path_namespaceObject.join(targetLibDir, 'package.json'));
|
|
12090
12738
|
this.logger.log(`Merged package.json files`);
|
|
12739
|
+
await external_fs_extra_namespaceObject.writeJson(external_path_namespaceObject.join(targetLibDir, 'hashes.json'), hashes, {
|
|
12740
|
+
spaces: 2
|
|
12741
|
+
});
|
|
12742
|
+
this.logger.log(`Wrote hashes.json to ${targetLibDir}`);
|
|
12091
12743
|
} catch (error) {
|
|
12092
12744
|
this.logger.error(`Failed to copy content to node_modules: ${error}`);
|
|
12093
12745
|
throw error;
|
|
@@ -12117,7 +12769,9 @@ class OperationsService {
|
|
|
12117
12769
|
await this.generatorBinding.generateSource(descriptors, source);
|
|
12118
12770
|
}
|
|
12119
12771
|
async getDescriptors(ctx, source) {
|
|
12120
|
-
|
|
12772
|
+
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12773
|
+
// Hash is now available for operations.service to use
|
|
12774
|
+
return descriptors;
|
|
12121
12775
|
}
|
|
12122
12776
|
async clearGenerateDir(ctx) {
|
|
12123
12777
|
if (external_fs_extra_namespaceObject.pathExistsSync(this.generateDir)) {
|
|
@@ -12136,6 +12790,74 @@ class OperationsService {
|
|
|
12136
12790
|
external_fs_extra_namespaceObject.ensureDirSync(this.generateDir);
|
|
12137
12791
|
return this.generateDir;
|
|
12138
12792
|
}
|
|
12793
|
+
async verify(hashes) {
|
|
12794
|
+
this.logger.log('Verifying hashes');
|
|
12795
|
+
try {
|
|
12796
|
+
// 1. Load intrigConfig
|
|
12797
|
+
const intrigConfig = this.configService.get();
|
|
12798
|
+
const sources = intrigConfig.sources || [];
|
|
12799
|
+
// 2. Check that each key in hashes has a matching source id
|
|
12800
|
+
const sourceIds = new Set(sources.map((source)=>source.id));
|
|
12801
|
+
const hashKeys = Object.keys(hashes);
|
|
12802
|
+
for (const hashKey of hashKeys){
|
|
12803
|
+
if (!sourceIds.has(hashKey)) {
|
|
12804
|
+
this.logger.error(`Hash key '${hashKey}' does not have a matching source id`);
|
|
12805
|
+
return {
|
|
12806
|
+
status: 'conflict',
|
|
12807
|
+
message: `Hash key '${hashKey}' does not have a matching source id`,
|
|
12808
|
+
statusCode: 409
|
|
12809
|
+
};
|
|
12810
|
+
}
|
|
12811
|
+
}
|
|
12812
|
+
// 3. Check that there are no more sources than hashes
|
|
12813
|
+
if (sources.length > hashKeys.length) {
|
|
12814
|
+
const extraSources = sources.map((s)=>s.id).filter((id)=>!hashKeys.includes(id));
|
|
12815
|
+
this.logger.error(`There are more sources than hashes. Extra sources: ${extraSources.join(', ')}`);
|
|
12816
|
+
return {
|
|
12817
|
+
status: 'conflict',
|
|
12818
|
+
message: `There are more sources than hashes. Extra sources: ${extraSources.join(', ')}`,
|
|
12819
|
+
statusCode: 409
|
|
12820
|
+
};
|
|
12821
|
+
}
|
|
12822
|
+
// 4. Get hash for each source and validate they match
|
|
12823
|
+
for (const source of sources){
|
|
12824
|
+
try {
|
|
12825
|
+
const actualHash = await this.openApiService.getHash(source.id);
|
|
12826
|
+
const expectedHash = hashes[source.id];
|
|
12827
|
+
if (actualHash !== expectedHash) {
|
|
12828
|
+
this.logger.error(`Hash mismatch for source '${source.id}'. Expected: ${expectedHash}, Actual: ${actualHash}`);
|
|
12829
|
+
return {
|
|
12830
|
+
status: 'conflict',
|
|
12831
|
+
message: `Hash mismatch for source '${source.id}'. Expected: ${expectedHash}, Actual: ${actualHash}`,
|
|
12832
|
+
statusCode: 409
|
|
12833
|
+
};
|
|
12834
|
+
}
|
|
12835
|
+
} catch (error) {
|
|
12836
|
+
this.logger.error(`Failed to get hash for source '${source.id}': ${error.message}`);
|
|
12837
|
+
return {
|
|
12838
|
+
status: 'conflict',
|
|
12839
|
+
message: `Failed to get hash for source '${source.id}': ${error.message}`,
|
|
12840
|
+
statusCode: 409
|
|
12841
|
+
};
|
|
12842
|
+
}
|
|
12843
|
+
}
|
|
12844
|
+
// 5. All validations passed
|
|
12845
|
+
this.logger.log('All hash validations passed successfully');
|
|
12846
|
+
return {
|
|
12847
|
+
status: 'verified',
|
|
12848
|
+
message: 'All hashes match successfully',
|
|
12849
|
+
hashCount: Object.keys(hashes).length,
|
|
12850
|
+
statusCode: 200
|
|
12851
|
+
};
|
|
12852
|
+
} catch (error) {
|
|
12853
|
+
this.logger.error(`Verification failed with error: ${error.message}`);
|
|
12854
|
+
return {
|
|
12855
|
+
status: 'conflict',
|
|
12856
|
+
message: `Verification failed: ${error.message}`,
|
|
12857
|
+
statusCode: 409
|
|
12858
|
+
};
|
|
12859
|
+
}
|
|
12860
|
+
}
|
|
12139
12861
|
}
|
|
12140
12862
|
operations_service_ts_decorate([
|
|
12141
12863
|
WithStatus((event)=>({
|
|
@@ -12203,7 +12925,8 @@ operations_service_ts_decorate([
|
|
|
12203
12925
|
})),
|
|
12204
12926
|
operations_service_ts_metadata("design:type", Function),
|
|
12205
12927
|
operations_service_ts_metadata("design:paramtypes", [
|
|
12206
|
-
typeof GenerateEventContext === "undefined" ? Object : GenerateEventContext
|
|
12928
|
+
typeof GenerateEventContext === "undefined" ? Object : GenerateEventContext,
|
|
12929
|
+
typeof Record === "undefined" ? Object : Record
|
|
12207
12930
|
]),
|
|
12208
12931
|
operations_service_ts_metadata("design:returntype", Promise)
|
|
12209
12932
|
], OperationsService.prototype, "copyContentToNodeModules", null);
|
|
@@ -12322,6 +13045,7 @@ function operations_controller_ts_param(paramIndex, decorator) {
|
|
|
12322
13045
|
|
|
12323
13046
|
|
|
12324
13047
|
|
|
13048
|
+
|
|
12325
13049
|
class OperationsController {
|
|
12326
13050
|
constructor(operationsService){
|
|
12327
13051
|
this.operationsService = operationsService;
|
|
@@ -12346,9 +13070,16 @@ class OperationsController {
|
|
|
12346
13070
|
await this.operationsService.generate(generateEventContext);
|
|
12347
13071
|
generateEventContext.done(new GenerateDoneEventDto(true));
|
|
12348
13072
|
this.logger.log('Generate operation completed');
|
|
12349
|
-
});
|
|
13073
|
+
}, 0);
|
|
12350
13074
|
return events$;
|
|
12351
13075
|
}
|
|
13076
|
+
async verify(hashes, res) {
|
|
13077
|
+
this.logger.log('Starting verify operation');
|
|
13078
|
+
const result = await this.operationsService.verify(hashes);
|
|
13079
|
+
this.logger.log('Verify operation completed');
|
|
13080
|
+
const statusCode = result.statusCode || 200;
|
|
13081
|
+
return res.status(statusCode).json(result);
|
|
13082
|
+
}
|
|
12352
13083
|
}
|
|
12353
13084
|
operations_controller_ts_decorate([
|
|
12354
13085
|
(0,common_namespaceObject.Sse)('sync'),
|
|
@@ -12401,6 +13132,25 @@ operations_controller_ts_decorate([
|
|
|
12401
13132
|
operations_controller_ts_metadata("design:paramtypes", []),
|
|
12402
13133
|
operations_controller_ts_metadata("design:returntype", Promise)
|
|
12403
13134
|
], OperationsController.prototype, "generate", null);
|
|
13135
|
+
operations_controller_ts_decorate([
|
|
13136
|
+
(0,common_namespaceObject.Post)('verify'),
|
|
13137
|
+
(0,swagger_namespaceObject.ApiResponse)({
|
|
13138
|
+
status: 200,
|
|
13139
|
+
description: 'Verify hashes - successful verification'
|
|
13140
|
+
}),
|
|
13141
|
+
(0,swagger_namespaceObject.ApiResponse)({
|
|
13142
|
+
status: 409,
|
|
13143
|
+
description: 'Verify hashes - validation conflicts found'
|
|
13144
|
+
}),
|
|
13145
|
+
operations_controller_ts_param(0, (0,common_namespaceObject.Body)()),
|
|
13146
|
+
operations_controller_ts_param(1, (0,common_namespaceObject.Res)()),
|
|
13147
|
+
operations_controller_ts_metadata("design:type", Function),
|
|
13148
|
+
operations_controller_ts_metadata("design:paramtypes", [
|
|
13149
|
+
typeof Record === "undefined" ? Object : Record,
|
|
13150
|
+
typeof external_express_namespaceObject.Response === "undefined" ? Object : external_express_namespaceObject.Response
|
|
13151
|
+
]),
|
|
13152
|
+
operations_controller_ts_metadata("design:returntype", Promise)
|
|
13153
|
+
], OperationsController.prototype, "verify", null);
|
|
12404
13154
|
OperationsController = operations_controller_ts_decorate([
|
|
12405
13155
|
(0,swagger_namespaceObject.ApiTags)('Operations'),
|
|
12406
13156
|
(0,swagger_namespaceObject.ApiExtraModels)(SyncStatusEventDto, SyncDoneEventDto, GenerateStatusEventDto, GenerateDoneEventDto),
|
|
@@ -13609,62 +14359,6 @@ DeamonModule = deamon_module_ts_decorate([
|
|
|
13609
14359
|
};
|
|
13610
14360
|
});
|
|
13611
14361
|
|
|
13612
|
-
;// external "url"
|
|
13613
|
-
const external_url_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("url");
|
|
13614
|
-
;// external "process"
|
|
13615
|
-
const external_process_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("process");
|
|
13616
|
-
;// ./src/app/debug/debug.controller.ts
|
|
13617
|
-
function debug_controller_ts_decorate(decorators, target, key, desc) {
|
|
13618
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
13619
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13620
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
13621
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
13622
|
-
}
|
|
13623
|
-
function debug_controller_ts_metadata(k, v) {
|
|
13624
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
13625
|
-
}
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
// Get the directory name in ES modules
|
|
13631
|
-
const debug_controller_filename = (0,external_url_namespaceObject.fileURLToPath)("file:///home/tiran-intrigsoft/IdeaProjects/intrig-core/app/intrig/src/app/debug/debug.controller.ts");
|
|
13632
|
-
const debug_controller_dirname = (0,external_path_namespaceObject.dirname)(debug_controller_filename);
|
|
13633
|
-
class DebugController {
|
|
13634
|
-
getDirname() {
|
|
13635
|
-
// Calculate paths that might be relevant for debugging
|
|
13636
|
-
const assetsPath = (0,external_path_namespaceObject.join)(external_process_namespaceObject.cwd(), 'dist', 'app', 'intrig', 'assets', 'insight');
|
|
13637
|
-
const appDirname = (0,external_path_namespaceObject.dirname)(debug_controller_dirname);
|
|
13638
|
-
return {
|
|
13639
|
-
// ES Module specific values
|
|
13640
|
-
dirname: debug_controller_dirname,
|
|
13641
|
-
filename: debug_controller_filename,
|
|
13642
|
-
meta_url: "file:///home/tiran-intrigsoft/IdeaProjects/intrig-core/app/intrig/src/app/debug/debug.controller.ts",
|
|
13643
|
-
// Calculated paths
|
|
13644
|
-
assetsPath,
|
|
13645
|
-
appDirname,
|
|
13646
|
-
// Process information
|
|
13647
|
-
cwd: external_process_namespaceObject.cwd(),
|
|
13648
|
-
execPath: external_process_namespaceObject.execPath,
|
|
13649
|
-
// Environment
|
|
13650
|
-
nodeEnv: external_process_namespaceObject.env.NODE_ENV,
|
|
13651
|
-
// Module type
|
|
13652
|
-
isESM: "function" === 'undefined',
|
|
13653
|
-
// Timestamp for cache busting
|
|
13654
|
-
timestamp: new Date().toISOString()
|
|
13655
|
-
};
|
|
13656
|
-
}
|
|
13657
|
-
}
|
|
13658
|
-
debug_controller_ts_decorate([
|
|
13659
|
-
(0,common_namespaceObject.Get)('dirname'),
|
|
13660
|
-
debug_controller_ts_metadata("design:type", Function),
|
|
13661
|
-
debug_controller_ts_metadata("design:paramtypes", []),
|
|
13662
|
-
debug_controller_ts_metadata("design:returntype", void 0)
|
|
13663
|
-
], DebugController.prototype, "getDirname", null);
|
|
13664
|
-
DebugController = debug_controller_ts_decorate([
|
|
13665
|
-
(0,common_namespaceObject.Controller)('debug')
|
|
13666
|
-
], DebugController);
|
|
13667
|
-
|
|
13668
14362
|
;// ./src/app/app.module.ts
|
|
13669
14363
|
function app_module_ts_decorate(decorators, target, key, desc) {
|
|
13670
14364
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -13679,8 +14373,6 @@ function app_module_ts_decorate(decorators, target, key, desc) {
|
|
|
13679
14373
|
|
|
13680
14374
|
|
|
13681
14375
|
|
|
13682
|
-
// import {McpModule} from "./mcp/mcp.module";
|
|
13683
|
-
|
|
13684
14376
|
class AppModule {
|
|
13685
14377
|
}
|
|
13686
14378
|
AppModule = app_module_ts_decorate([
|
|
@@ -13697,9 +14389,7 @@ AppModule = app_module_ts_decorate([
|
|
|
13697
14389
|
DiscoveryModule,
|
|
13698
14390
|
DeamonModule
|
|
13699
14391
|
],
|
|
13700
|
-
controllers: [
|
|
13701
|
-
DebugController
|
|
13702
|
-
],
|
|
14392
|
+
controllers: [],
|
|
13703
14393
|
providers: []
|
|
13704
14394
|
})
|
|
13705
14395
|
], AppModule);
|
|
@@ -13717,12 +14407,10 @@ const core_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("
|
|
|
13717
14407
|
|
|
13718
14408
|
|
|
13719
14409
|
|
|
14410
|
+
|
|
13720
14411
|
const logger = new common_namespaceObject.Logger('Main');
|
|
13721
14412
|
async function bootstrapDeamon() {
|
|
13722
14413
|
const app = await core_namespaceObject.NestFactory.create(AppModule, {
|
|
13723
|
-
logger: [
|
|
13724
|
-
'error'
|
|
13725
|
-
]
|
|
13726
14414
|
});
|
|
13727
14415
|
app.enableShutdownHooks();
|
|
13728
14416
|
app.enableCors();
|
|
@@ -13760,6 +14448,14 @@ async function bootstrap() {
|
|
|
13760
14448
|
await app.init();
|
|
13761
14449
|
process.stdin.resume();
|
|
13762
14450
|
} else {
|
|
14451
|
+
if (cmd === 'init') {
|
|
14452
|
+
const configPath = './intrig.config.json';
|
|
14453
|
+
if (external_fs_namespaceObject.existsSync(configPath)) {
|
|
14454
|
+
throw new Error('Intrig is already initialized. Configuration file exists.');
|
|
14455
|
+
}
|
|
14456
|
+
external_fs_namespaceObject.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
14457
|
+
logger?.log('Initialized new Intrig project');
|
|
14458
|
+
}
|
|
13763
14459
|
try {
|
|
13764
14460
|
await external_nest_commander_namespaceObject.CommandFactory.run(AppModule, {
|
|
13765
14461
|
logger: false,
|