@intrig/core 0.0.15-4 → 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 +883 -193
- 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
|
}
|
|
@@ -5528,84 +5610,533 @@ ${"```"}
|
|
|
5528
5610
|
<summary>Early Return</summary>
|
|
5529
5611
|
Often times you may need to show the loading indicators while the actual network call is in progress. In that case, you can add a conditional early return.
|
|
5530
5612
|
|
|
5531
|
-
${"```tsx"}
|
|
5613
|
+
${"```tsx"}
|
|
5614
|
+
if (isPending(${camelCase(descriptor.name)}Resp)) {
|
|
5615
|
+
return <LoadingIndicator />
|
|
5616
|
+
}
|
|
5617
|
+
${"```"}
|
|
5618
|
+
</details>
|
|
5619
|
+
|
|
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
|
+
|
|
5532
6033
|
if (isPending(${camelCase(descriptor.name)}Resp)) {
|
|
5533
|
-
return
|
|
6034
|
+
return <>{data.map(a => <>{JSON.stringify(a)}</>)}</>
|
|
5534
6035
|
}
|
|
5535
|
-
${"```"}
|
|
5536
|
-
</details>
|
|
5537
6036
|
|
|
5538
|
-
|
|
5539
|
-
<
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
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
|
+
}
|
|
5544
6045
|
${"```"}
|
|
5545
6046
|
</details>
|
|
5546
6047
|
|
|
5547
|
-
|
|
5548
|
-
|
|
6048
|
+
`;
|
|
6049
|
+
}
|
|
5549
6050
|
|
|
5550
|
-
|
|
5551
|
-
import { isError } from '@intrig/next';
|
|
5552
|
-
${"```"}
|
|
6051
|
+
;// ../../lib/next-binding/src/lib/templates/docs/async-hook.ts
|
|
5553
6052
|
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
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).
|
|
5557
6061
|
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
}
|
|
6062
|
+
## Imports
|
|
6063
|
+
|
|
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
|
-
|
|
5574
|
-
<details>
|
|
5575
|
-
<summary>Side effect upon error</summary>
|
|
5576
6074
|
|
|
5577
|
-
|
|
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])
|
|
@@ -8097,6 +8654,15 @@ async function reactDownloadHookTemplate({ source, data: { paths, operationId, r
|
|
|
8097
8654
|
"...params"
|
|
8098
8655
|
].join(",");
|
|
8099
8656
|
const finalRequestBodyBlock = requestBody ? `,data: encode(data, "${contentType}", requestBodySchema)` : '';
|
|
8657
|
+
function responseTypePart() {
|
|
8658
|
+
switch(responseType){
|
|
8659
|
+
case "application/octet-stream":
|
|
8660
|
+
return `responseType: 'blob', adapter: 'fetch',`;
|
|
8661
|
+
case "text/event-stream":
|
|
8662
|
+
return `responseType: 'stream', adapter: 'fetch',`;
|
|
8663
|
+
}
|
|
8664
|
+
return '';
|
|
8665
|
+
}
|
|
8100
8666
|
return ts`
|
|
8101
8667
|
${[
|
|
8102
8668
|
...imports
|
|
@@ -8175,7 +8741,7 @@ async function reactDownloadHookTemplate({ source, data: { paths, operationId, r
|
|
|
8175
8741
|
key: \`${"${source}: ${operation}"}\`,
|
|
8176
8742
|
source: '${source}'
|
|
8177
8743
|
${requestBody ? finalRequestBodyBlock : ''},
|
|
8178
|
-
${
|
|
8744
|
+
${responseTypePart()}
|
|
8179
8745
|
})
|
|
8180
8746
|
return successfulDispatch();
|
|
8181
8747
|
}, [dispatch])
|
|
@@ -9068,7 +9634,7 @@ ${"```"}
|
|
|
9068
9634
|
;// ../../lib/react-binding/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts
|
|
9069
9635
|
|
|
9070
9636
|
|
|
9071
|
-
function
|
|
9637
|
+
function asyncFunctionHook_template_extractAsyncHookShape(response, requestBody, imports) {
|
|
9072
9638
|
if (response) {
|
|
9073
9639
|
if (requestBody) {
|
|
9074
9640
|
imports.add(`import { BinaryFunctionAsyncHook } from "@intrig/react"`);
|
|
@@ -9087,7 +9653,7 @@ function extractAsyncHookShape(response, requestBody, imports) {
|
|
|
9087
9653
|
}
|
|
9088
9654
|
}
|
|
9089
9655
|
}
|
|
9090
|
-
function
|
|
9656
|
+
function method_asyncFunctionHook_template_extractErrorParams(errorTypes) {
|
|
9091
9657
|
switch(errorTypes.length){
|
|
9092
9658
|
case 0:
|
|
9093
9659
|
return `
|
|
@@ -9103,7 +9669,7 @@ function asyncFunctionHook_template_extractErrorParams(errorTypes) {
|
|
|
9103
9669
|
const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
|
|
9104
9670
|
}
|
|
9105
9671
|
}
|
|
9106
|
-
function
|
|
9672
|
+
function method_asyncFunctionHook_template_extractParamDeconstruction(variables, requestBody) {
|
|
9107
9673
|
const isParamMandatory = variables?.some((a)=>a.in === 'path') || false;
|
|
9108
9674
|
if (requestBody) {
|
|
9109
9675
|
if (isParamMandatory) {
|
|
@@ -9141,7 +9707,7 @@ async function reactAsyncFunctionHookTemplate({ source, data: { paths, operation
|
|
|
9141
9707
|
imports.add(`import { useCallback } from 'react'`);
|
|
9142
9708
|
imports.add(`import { useTransientCall, encode, isError, isSuccess } from '@intrig/react'`);
|
|
9143
9709
|
// Hook signature type
|
|
9144
|
-
const hookShape =
|
|
9710
|
+
const hookShape = asyncFunctionHook_template_extractAsyncHookShape(response, requestBody, imports);
|
|
9145
9711
|
// Add body/response param imports
|
|
9146
9712
|
if (requestBody) {
|
|
9147
9713
|
imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/react/${source}/components/schemas/${requestBody}"`);
|
|
@@ -9156,14 +9722,23 @@ async function reactAsyncFunctionHookTemplate({ source, data: { paths, operation
|
|
|
9156
9722
|
];
|
|
9157
9723
|
errorTypes.forEach((ref)=>imports.add(`import { ${ref}, ${ref}Schema } from "@intrig/react/${source}/components/schemas/${ref}"`));
|
|
9158
9724
|
// Error schema block
|
|
9159
|
-
const errorSchemaBlock =
|
|
9725
|
+
const errorSchemaBlock = method_asyncFunctionHook_template_extractErrorParams(errorTypes.map((a)=>a));
|
|
9160
9726
|
// Param deconstruction
|
|
9161
|
-
const { paramExpression, paramType } =
|
|
9727
|
+
const { paramExpression, paramType } = method_asyncFunctionHook_template_extractParamDeconstruction(variables ?? [], requestBody);
|
|
9162
9728
|
const paramExplode = [
|
|
9163
9729
|
...variables?.filter((a)=>a.in === 'path').map((a)=>a.name) ?? [],
|
|
9164
9730
|
'...params'
|
|
9165
9731
|
].join(',');
|
|
9166
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
|
+
}
|
|
9167
9742
|
return ts`
|
|
9168
9743
|
${[
|
|
9169
9744
|
...imports
|
|
@@ -9205,7 +9780,7 @@ function use${pascalCase(operationId)}AsyncHook(): [(${paramType}) => Promise<Re
|
|
|
9205
9780
|
key: \`${"${source}: ${operation}"}\`,
|
|
9206
9781
|
source: '${source}'
|
|
9207
9782
|
${requestBody ? finalRequestBodyBlock : ''},
|
|
9208
|
-
${
|
|
9783
|
+
${responseTypePart()}
|
|
9209
9784
|
});
|
|
9210
9785
|
}, [call]);
|
|
9211
9786
|
|
|
@@ -9312,27 +9887,12 @@ ${"```"}
|
|
|
9312
9887
|
;// ../../lib/react-binding/src/lib/templates/type-utils.template.ts
|
|
9313
9888
|
|
|
9314
9889
|
|
|
9315
|
-
function
|
|
9890
|
+
function type_utils_template_typeUtilsTemplate(_path) {
|
|
9316
9891
|
const ts = typescript(external_path_default().resolve(_path, 'src', 'type-utils.ts'));
|
|
9317
9892
|
return ts`import { z } from 'zod';
|
|
9318
9893
|
|
|
9319
|
-
export type BinaryData = Blob
|
|
9320
|
-
|
|
9321
|
-
export const BinaryDataSchema = z.union([
|
|
9322
|
-
// Blob in browsers
|
|
9323
|
-
z.instanceof(Blob).optional(), // optional here so union below still validates if Blob is absent in Node
|
|
9324
|
-
// Raw buffers
|
|
9325
|
-
z.instanceof(ArrayBuffer),
|
|
9326
|
-
z.custom<Uint8Array>((v) => v instanceof Uint8Array, { message: 'Expected Uint8Array' }),
|
|
9327
|
-
]).transform((v) => {
|
|
9328
|
-
// Normalize to Blob if possible (nice for downloads in browser)
|
|
9329
|
-
if (typeof Blob !== 'undefined') {
|
|
9330
|
-
if (v instanceof Blob) return v;
|
|
9331
|
-
if (v instanceof ArrayBuffer) return new Blob([v]);
|
|
9332
|
-
if (v instanceof Uint8Array) return new Blob([v.buffer]);
|
|
9333
|
-
}
|
|
9334
|
-
return v;
|
|
9335
|
-
});
|
|
9894
|
+
export type BinaryData = Blob;
|
|
9895
|
+
export const BinaryDataSchema: z.ZodType<BinaryData> = z.instanceof(Blob);
|
|
9336
9896
|
|
|
9337
9897
|
// Base64 helpers (browser + Node compatible; no Buffer required)
|
|
9338
9898
|
export function base64ToUint8Array(b64: string): Uint8Array {
|
|
@@ -9412,7 +9972,7 @@ class ReactBindingService extends GeneratorBinding {
|
|
|
9412
9972
|
await this.dump(reactPackageJsonTemplate(this._path));
|
|
9413
9973
|
await this.dump(reactProviderTemplate(this._path, apisToSync));
|
|
9414
9974
|
await this.dump(reactTsConfigTemplate(this._path));
|
|
9415
|
-
await this.dump(
|
|
9975
|
+
await this.dump(type_utils_template_typeUtilsTemplate(this._path));
|
|
9416
9976
|
}
|
|
9417
9977
|
async generateSource(descriptors, source) {
|
|
9418
9978
|
//TODO improve this logic to catch potential conflicts.
|
|
@@ -10742,10 +11302,13 @@ function deref(spec) {
|
|
|
10742
11302
|
};
|
|
10743
11303
|
}
|
|
10744
11304
|
|
|
11305
|
+
;// external "node:crypto"
|
|
11306
|
+
const external_node_crypto_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:crypto");
|
|
10745
11307
|
;// ../../lib/openapi-source/src/lib/util/normalize.ts
|
|
10746
11308
|
|
|
10747
11309
|
|
|
10748
11310
|
|
|
11311
|
+
|
|
10749
11312
|
function generateTypeName(operationOb, postFix) {
|
|
10750
11313
|
return [
|
|
10751
11314
|
operationOb.tags?.[0],
|
|
@@ -10755,7 +11318,7 @@ function generateTypeName(operationOb, postFix) {
|
|
|
10755
11318
|
}
|
|
10756
11319
|
function normalize(spec) {
|
|
10757
11320
|
const doDeref = deref(spec);
|
|
10758
|
-
|
|
11321
|
+
const normalized = (0,external_immer_namespaceObject.produce)(spec, (draft)=>{
|
|
10759
11322
|
const paths = draft.paths;
|
|
10760
11323
|
for (const [path, pathItem] of Object.entries(paths)){
|
|
10761
11324
|
const pathItemObject = pathItem;
|
|
@@ -10860,6 +11423,9 @@ function normalize(spec) {
|
|
|
10860
11423
|
}
|
|
10861
11424
|
//TODO implement fix schema types.
|
|
10862
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
|
+
});
|
|
10863
11429
|
}
|
|
10864
11430
|
|
|
10865
11431
|
;// ../../lib/openapi-source/src/lib/util/extractRequestsFromSpec.ts
|
|
@@ -11003,26 +11569,47 @@ class IntrigOpenapiService {
|
|
|
11003
11569
|
}
|
|
11004
11570
|
async sync(config, id, ctx) {
|
|
11005
11571
|
this.logger.log('Starting OpenAPI sync process');
|
|
11572
|
+
const errors = [];
|
|
11006
11573
|
if (!id) {
|
|
11574
|
+
// Process all sources with better error isolation
|
|
11007
11575
|
for (const source of config.sources){
|
|
11008
11576
|
this.logger.log(`Processing source: ${source.id}`);
|
|
11009
11577
|
try {
|
|
11010
11578
|
await this.doSync(source, config, ctx);
|
|
11579
|
+
this.logger.log(`Successfully synced source: ${source.id}`);
|
|
11011
11580
|
} catch (e) {
|
|
11012
|
-
|
|
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
|
|
11013
11588
|
}
|
|
11014
11589
|
}
|
|
11015
11590
|
} else {
|
|
11016
11591
|
const source = config.sources.find((s)=>s.id === id);
|
|
11017
11592
|
if (!source) {
|
|
11018
|
-
|
|
11019
|
-
|
|
11593
|
+
const error = `Source ${id} not found`;
|
|
11594
|
+
this.logger.error(error);
|
|
11595
|
+
throw new Error(error);
|
|
11020
11596
|
}
|
|
11021
11597
|
this.logger.log(`Processing specific source: ${id}`);
|
|
11022
11598
|
try {
|
|
11023
11599
|
await this.doSync(source, config, ctx);
|
|
11600
|
+
this.logger.log(`Successfully synced source: ${id}`);
|
|
11024
11601
|
} catch (e) {
|
|
11025
|
-
|
|
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('; ')}`);
|
|
11026
11613
|
}
|
|
11027
11614
|
}
|
|
11028
11615
|
this.logger.log('OpenAPI sync process completed');
|
|
@@ -11068,7 +11655,8 @@ class IntrigOpenapiService {
|
|
|
11068
11655
|
const restData = extractRequestsFromSpec(document);
|
|
11069
11656
|
const schemas = extractSchemas(document);
|
|
11070
11657
|
const sha1 = (str)=>external_crypto_namespaceObject.createHash('sha1').update(str).digest('hex');
|
|
11071
|
-
|
|
11658
|
+
const hash = document.info['x-intrig-hash'];
|
|
11659
|
+
const descriptors = [
|
|
11072
11660
|
...restData.map((restData)=>ResourceDescriptor.from({
|
|
11073
11661
|
id: sha1(`${id}_${restData.method}_${restData.paths.join('_')}_${restData.operationId}_${restData.contentType}_${restData.responseType}`),
|
|
11074
11662
|
name: camelCase(restData.operationId),
|
|
@@ -11086,6 +11674,17 @@ class IntrigOpenapiService {
|
|
|
11086
11674
|
data: schema
|
|
11087
11675
|
}))
|
|
11088
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'];
|
|
11089
11688
|
}
|
|
11090
11689
|
validate(normalized, restOptions) {
|
|
11091
11690
|
function validateForConflictingVariables() {
|
|
@@ -11676,7 +12275,7 @@ class SearchService {
|
|
|
11676
12275
|
async onModuleInit() {
|
|
11677
12276
|
try {
|
|
11678
12277
|
for (const source of this.configService.get().sources){
|
|
11679
|
-
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12278
|
+
const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
|
|
11680
12279
|
descriptors.forEach((descriptor)=>this.addDescriptor(descriptor));
|
|
11681
12280
|
}
|
|
11682
12281
|
// Update the CodeAnalyzer with all descriptors
|
|
@@ -11968,24 +12567,50 @@ class OperationsService {
|
|
|
11968
12567
|
this.config = config;
|
|
11969
12568
|
this.searchService = searchService;
|
|
11970
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
|
|
11971
12573
|
this.generateDir = this.config.get("generatedDir") ?? external_path_namespaceObject.resolve(process.cwd(), ".intrig", "generated");
|
|
11972
12574
|
}
|
|
11973
12575
|
async getConfig(ctx) {
|
|
11974
12576
|
return this.configService.get();
|
|
11975
12577
|
}
|
|
11976
12578
|
async sync(ctx, id) {
|
|
11977
|
-
const
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
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
|
+
}
|
|
11989
12614
|
}
|
|
11990
12615
|
async indexDiff(ctx, prevDescriptors, newDescriptors) {
|
|
11991
12616
|
this.searchService.clearAll();
|
|
@@ -11993,21 +12618,36 @@ class OperationsService {
|
|
|
11993
12618
|
}
|
|
11994
12619
|
async getPreviousState(ctx, config) {
|
|
11995
12620
|
let prevDescriptors = [];
|
|
12621
|
+
const errors = [];
|
|
11996
12622
|
for (const source of config.sources){
|
|
11997
12623
|
try {
|
|
11998
|
-
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12624
|
+
const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
|
|
11999
12625
|
prevDescriptors = [
|
|
12000
12626
|
...prevDescriptors,
|
|
12001
12627
|
...descriptors
|
|
12002
12628
|
];
|
|
12003
|
-
|
|
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('; ')}`);
|
|
12004
12642
|
}
|
|
12643
|
+
this.logger.log(`Loaded total of ${prevDescriptors.length} previous descriptors`);
|
|
12005
12644
|
return prevDescriptors;
|
|
12006
12645
|
}
|
|
12007
12646
|
async getNewState(ctx, config) {
|
|
12647
|
+
this.logger.log('Loading new state');
|
|
12008
12648
|
let prevDescriptors = [];
|
|
12009
12649
|
for (const source of config.sources){
|
|
12010
|
-
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12650
|
+
const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
|
|
12011
12651
|
prevDescriptors = [
|
|
12012
12652
|
...prevDescriptors,
|
|
12013
12653
|
...descriptors
|
|
@@ -12039,8 +12679,10 @@ class OperationsService {
|
|
|
12039
12679
|
async generate(ctx) {
|
|
12040
12680
|
const config = await this.getConfig(ctx);
|
|
12041
12681
|
await this.clearGenerateDir(ctx);
|
|
12682
|
+
const hashes = {};
|
|
12042
12683
|
for (const source of config.sources){
|
|
12043
|
-
const descriptors = await this.getDescriptors(ctx, source);
|
|
12684
|
+
const { descriptors, hash } = await this.getDescriptors(ctx, source);
|
|
12685
|
+
hashes[source.id] = hash;
|
|
12044
12686
|
await this.generateSourceContent(ctx, descriptors, source);
|
|
12045
12687
|
}
|
|
12046
12688
|
await this.generateGlobalContent(ctx, config.sources);
|
|
@@ -12048,9 +12690,9 @@ class OperationsService {
|
|
|
12048
12690
|
await this.buildContent(ctx);
|
|
12049
12691
|
const tempBuildContext = config.__dangorouslyOverrideBuild;
|
|
12050
12692
|
if (tempBuildContext) {
|
|
12051
|
-
this.copyContentToSource(ctx, tempBuildContext);
|
|
12693
|
+
await this.copyContentToSource(ctx, tempBuildContext);
|
|
12052
12694
|
} else {
|
|
12053
|
-
await this.copyContentToNodeModules(ctx);
|
|
12695
|
+
await this.copyContentToNodeModules(ctx, hashes);
|
|
12054
12696
|
}
|
|
12055
12697
|
await this.executePostBuild(ctx);
|
|
12056
12698
|
}
|
|
@@ -12082,7 +12724,7 @@ class OperationsService {
|
|
|
12082
12724
|
throw error;
|
|
12083
12725
|
}
|
|
12084
12726
|
}
|
|
12085
|
-
async copyContentToNodeModules(ctx) {
|
|
12727
|
+
async copyContentToNodeModules(ctx, hashes) {
|
|
12086
12728
|
const targetLibDir = external_path_namespaceObject.join(this.config.get('rootDir') ?? process.cwd(), 'node_modules', '@intrig', this.generatorBinding.getLibName());
|
|
12087
12729
|
try {
|
|
12088
12730
|
if (await external_fs_extra_namespaceObject.pathExists(external_path_namespaceObject.join(targetLibDir, 'src'))) {
|
|
@@ -12094,6 +12736,10 @@ class OperationsService {
|
|
|
12094
12736
|
this.logger.log(`Copied ${targetLibDir}`);
|
|
12095
12737
|
await this.mergePackageJson(external_path_namespaceObject.join(this.generateDir, 'package.json'), external_path_namespaceObject.join(targetLibDir, 'package.json'));
|
|
12096
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}`);
|
|
12097
12743
|
} catch (error) {
|
|
12098
12744
|
this.logger.error(`Failed to copy content to node_modules: ${error}`);
|
|
12099
12745
|
throw error;
|
|
@@ -12123,7 +12769,9 @@ class OperationsService {
|
|
|
12123
12769
|
await this.generatorBinding.generateSource(descriptors, source);
|
|
12124
12770
|
}
|
|
12125
12771
|
async getDescriptors(ctx, source) {
|
|
12126
|
-
|
|
12772
|
+
const descriptors = await this.openApiService.getResourceDescriptors(source.id);
|
|
12773
|
+
// Hash is now available for operations.service to use
|
|
12774
|
+
return descriptors;
|
|
12127
12775
|
}
|
|
12128
12776
|
async clearGenerateDir(ctx) {
|
|
12129
12777
|
if (external_fs_extra_namespaceObject.pathExistsSync(this.generateDir)) {
|
|
@@ -12142,6 +12790,74 @@ class OperationsService {
|
|
|
12142
12790
|
external_fs_extra_namespaceObject.ensureDirSync(this.generateDir);
|
|
12143
12791
|
return this.generateDir;
|
|
12144
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
|
+
}
|
|
12145
12861
|
}
|
|
12146
12862
|
operations_service_ts_decorate([
|
|
12147
12863
|
WithStatus((event)=>({
|
|
@@ -12209,7 +12925,8 @@ operations_service_ts_decorate([
|
|
|
12209
12925
|
})),
|
|
12210
12926
|
operations_service_ts_metadata("design:type", Function),
|
|
12211
12927
|
operations_service_ts_metadata("design:paramtypes", [
|
|
12212
|
-
typeof GenerateEventContext === "undefined" ? Object : GenerateEventContext
|
|
12928
|
+
typeof GenerateEventContext === "undefined" ? Object : GenerateEventContext,
|
|
12929
|
+
typeof Record === "undefined" ? Object : Record
|
|
12213
12930
|
]),
|
|
12214
12931
|
operations_service_ts_metadata("design:returntype", Promise)
|
|
12215
12932
|
], OperationsService.prototype, "copyContentToNodeModules", null);
|
|
@@ -12328,6 +13045,7 @@ function operations_controller_ts_param(paramIndex, decorator) {
|
|
|
12328
13045
|
|
|
12329
13046
|
|
|
12330
13047
|
|
|
13048
|
+
|
|
12331
13049
|
class OperationsController {
|
|
12332
13050
|
constructor(operationsService){
|
|
12333
13051
|
this.operationsService = operationsService;
|
|
@@ -12352,9 +13070,16 @@ class OperationsController {
|
|
|
12352
13070
|
await this.operationsService.generate(generateEventContext);
|
|
12353
13071
|
generateEventContext.done(new GenerateDoneEventDto(true));
|
|
12354
13072
|
this.logger.log('Generate operation completed');
|
|
12355
|
-
});
|
|
13073
|
+
}, 0);
|
|
12356
13074
|
return events$;
|
|
12357
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
|
+
}
|
|
12358
13083
|
}
|
|
12359
13084
|
operations_controller_ts_decorate([
|
|
12360
13085
|
(0,common_namespaceObject.Sse)('sync'),
|
|
@@ -12407,6 +13132,25 @@ operations_controller_ts_decorate([
|
|
|
12407
13132
|
operations_controller_ts_metadata("design:paramtypes", []),
|
|
12408
13133
|
operations_controller_ts_metadata("design:returntype", Promise)
|
|
12409
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);
|
|
12410
13154
|
OperationsController = operations_controller_ts_decorate([
|
|
12411
13155
|
(0,swagger_namespaceObject.ApiTags)('Operations'),
|
|
12412
13156
|
(0,swagger_namespaceObject.ApiExtraModels)(SyncStatusEventDto, SyncDoneEventDto, GenerateStatusEventDto, GenerateDoneEventDto),
|
|
@@ -13615,62 +14359,6 @@ DeamonModule = deamon_module_ts_decorate([
|
|
|
13615
14359
|
};
|
|
13616
14360
|
});
|
|
13617
14361
|
|
|
13618
|
-
;// external "url"
|
|
13619
|
-
const external_url_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("url");
|
|
13620
|
-
;// external "process"
|
|
13621
|
-
const external_process_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("process");
|
|
13622
|
-
;// ./src/app/debug/debug.controller.ts
|
|
13623
|
-
function debug_controller_ts_decorate(decorators, target, key, desc) {
|
|
13624
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
13625
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13626
|
-
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;
|
|
13627
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
13628
|
-
}
|
|
13629
|
-
function debug_controller_ts_metadata(k, v) {
|
|
13630
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
13631
|
-
}
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
// Get the directory name in ES modules
|
|
13637
|
-
const debug_controller_filename = (0,external_url_namespaceObject.fileURLToPath)("file:///home/tiran-intrigsoft/IdeaProjects/intrig-core/app/intrig/src/app/debug/debug.controller.ts");
|
|
13638
|
-
const debug_controller_dirname = (0,external_path_namespaceObject.dirname)(debug_controller_filename);
|
|
13639
|
-
class DebugController {
|
|
13640
|
-
getDirname() {
|
|
13641
|
-
// Calculate paths that might be relevant for debugging
|
|
13642
|
-
const assetsPath = (0,external_path_namespaceObject.join)(external_process_namespaceObject.cwd(), 'dist', 'app', 'intrig', 'assets', 'insight');
|
|
13643
|
-
const appDirname = (0,external_path_namespaceObject.dirname)(debug_controller_dirname);
|
|
13644
|
-
return {
|
|
13645
|
-
// ES Module specific values
|
|
13646
|
-
dirname: debug_controller_dirname,
|
|
13647
|
-
filename: debug_controller_filename,
|
|
13648
|
-
meta_url: "file:///home/tiran-intrigsoft/IdeaProjects/intrig-core/app/intrig/src/app/debug/debug.controller.ts",
|
|
13649
|
-
// Calculated paths
|
|
13650
|
-
assetsPath,
|
|
13651
|
-
appDirname,
|
|
13652
|
-
// Process information
|
|
13653
|
-
cwd: external_process_namespaceObject.cwd(),
|
|
13654
|
-
execPath: external_process_namespaceObject.execPath,
|
|
13655
|
-
// Environment
|
|
13656
|
-
nodeEnv: external_process_namespaceObject.env.NODE_ENV,
|
|
13657
|
-
// Module type
|
|
13658
|
-
isESM: "function" === 'undefined',
|
|
13659
|
-
// Timestamp for cache busting
|
|
13660
|
-
timestamp: new Date().toISOString()
|
|
13661
|
-
};
|
|
13662
|
-
}
|
|
13663
|
-
}
|
|
13664
|
-
debug_controller_ts_decorate([
|
|
13665
|
-
(0,common_namespaceObject.Get)('dirname'),
|
|
13666
|
-
debug_controller_ts_metadata("design:type", Function),
|
|
13667
|
-
debug_controller_ts_metadata("design:paramtypes", []),
|
|
13668
|
-
debug_controller_ts_metadata("design:returntype", void 0)
|
|
13669
|
-
], DebugController.prototype, "getDirname", null);
|
|
13670
|
-
DebugController = debug_controller_ts_decorate([
|
|
13671
|
-
(0,common_namespaceObject.Controller)('debug')
|
|
13672
|
-
], DebugController);
|
|
13673
|
-
|
|
13674
14362
|
;// ./src/app/app.module.ts
|
|
13675
14363
|
function app_module_ts_decorate(decorators, target, key, desc) {
|
|
13676
14364
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -13685,8 +14373,6 @@ function app_module_ts_decorate(decorators, target, key, desc) {
|
|
|
13685
14373
|
|
|
13686
14374
|
|
|
13687
14375
|
|
|
13688
|
-
// import {McpModule} from "./mcp/mcp.module";
|
|
13689
|
-
|
|
13690
14376
|
class AppModule {
|
|
13691
14377
|
}
|
|
13692
14378
|
AppModule = app_module_ts_decorate([
|
|
@@ -13703,9 +14389,7 @@ AppModule = app_module_ts_decorate([
|
|
|
13703
14389
|
DiscoveryModule,
|
|
13704
14390
|
DeamonModule
|
|
13705
14391
|
],
|
|
13706
|
-
controllers: [
|
|
13707
|
-
DebugController
|
|
13708
|
-
],
|
|
14392
|
+
controllers: [],
|
|
13709
14393
|
providers: []
|
|
13710
14394
|
})
|
|
13711
14395
|
], AppModule);
|
|
@@ -13723,12 +14407,10 @@ const core_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("
|
|
|
13723
14407
|
|
|
13724
14408
|
|
|
13725
14409
|
|
|
14410
|
+
|
|
13726
14411
|
const logger = new common_namespaceObject.Logger('Main');
|
|
13727
14412
|
async function bootstrapDeamon() {
|
|
13728
14413
|
const app = await core_namespaceObject.NestFactory.create(AppModule, {
|
|
13729
|
-
logger: [
|
|
13730
|
-
'error'
|
|
13731
|
-
]
|
|
13732
14414
|
});
|
|
13733
14415
|
app.enableShutdownHooks();
|
|
13734
14416
|
app.enableCors();
|
|
@@ -13766,6 +14448,14 @@ async function bootstrap() {
|
|
|
13766
14448
|
await app.init();
|
|
13767
14449
|
process.stdin.resume();
|
|
13768
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
|
+
}
|
|
13769
14459
|
try {
|
|
13770
14460
|
await external_nest_commander_namespaceObject.CommandFactory.run(AppModule, {
|
|
13771
14461
|
logger: false,
|