@codexsploitx/schemaapi 1.1.11 ā 1.1.13
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/dist/cli/templates/sdk.d.ts +1 -0
- package/dist/cli/templates/tests.d.ts +1 -0
- package/dist/cli.js +246 -115
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +105 -217
- package/website/docs.html +183 -0
- package/website/index.html +144 -0
- package/website/styles.css +417 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateSdkClient(name: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateTest(name: string): string;
|
package/dist/cli.js
CHANGED
|
@@ -6854,6 +6854,208 @@ export const { GET, POST, PUT, DELETE } = handlers;
|
|
|
6854
6854
|
`;
|
|
6855
6855
|
}
|
|
6856
6856
|
|
|
6857
|
+
function generateSdkClient(name) {
|
|
6858
|
+
const pascalName = toPascalCase(name);
|
|
6859
|
+
const camelName = toCamelCase(name);
|
|
6860
|
+
return `import {
|
|
6861
|
+
Create${pascalName}Request,
|
|
6862
|
+
Create${pascalName}Response,
|
|
6863
|
+
Get${pascalName}Response,
|
|
6864
|
+
Update${pascalName}Request,
|
|
6865
|
+
Update${pascalName}Response,
|
|
6866
|
+
Delete${pascalName}Response,
|
|
6867
|
+
List${pascalName}sResponse
|
|
6868
|
+
} from '../../contracts/${name}Contract';
|
|
6869
|
+
|
|
6870
|
+
export class ${pascalName}Client {
|
|
6871
|
+
private baseUrl: string;
|
|
6872
|
+
private headers: Record<string, string>;
|
|
6873
|
+
|
|
6874
|
+
constructor(baseUrl: string, token?: string) {
|
|
6875
|
+
this.baseUrl = baseUrl;
|
|
6876
|
+
this.headers = {
|
|
6877
|
+
'Content-Type': 'application/json',
|
|
6878
|
+
...(token ? { Authorization: \`Bearer \${token}\` } : {}),
|
|
6879
|
+
};
|
|
6880
|
+
}
|
|
6881
|
+
|
|
6882
|
+
/**
|
|
6883
|
+
* Create a new ${camelName}
|
|
6884
|
+
*/
|
|
6885
|
+
async create(data: Create${pascalName}Request): Promise<Create${pascalName}Response> {
|
|
6886
|
+
const response = await fetch(\`\${this.baseUrl}/${name}\`, {
|
|
6887
|
+
method: 'POST',
|
|
6888
|
+
headers: this.headers,
|
|
6889
|
+
body: JSON.stringify(data),
|
|
6890
|
+
});
|
|
6891
|
+
|
|
6892
|
+
if (!response.ok) {
|
|
6893
|
+
throw new Error(\`Failed to create ${camelName}: \${response.statusText}\`);
|
|
6894
|
+
}
|
|
6895
|
+
|
|
6896
|
+
return response.json();
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6899
|
+
/**
|
|
6900
|
+
* Get a ${camelName} by ID
|
|
6901
|
+
*/
|
|
6902
|
+
async get(id: string): Promise<Get${pascalName}Response> {
|
|
6903
|
+
const response = await fetch(\`\${this.baseUrl}/${name}/\${id}\`, {
|
|
6904
|
+
method: 'GET',
|
|
6905
|
+
headers: this.headers,
|
|
6906
|
+
});
|
|
6907
|
+
|
|
6908
|
+
if (!response.ok) {
|
|
6909
|
+
throw new Error(\`Failed to get ${camelName}: \${response.statusText}\`);
|
|
6910
|
+
}
|
|
6911
|
+
|
|
6912
|
+
return response.json();
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6915
|
+
/**
|
|
6916
|
+
* Update a ${camelName}
|
|
6917
|
+
*/
|
|
6918
|
+
async update(id: string, data: Update${pascalName}Request): Promise<Update${pascalName}Response> {
|
|
6919
|
+
const response = await fetch(\`\${this.baseUrl}/${name}/\${id}\`, {
|
|
6920
|
+
method: 'PUT',
|
|
6921
|
+
headers: this.headers,
|
|
6922
|
+
body: JSON.stringify(data),
|
|
6923
|
+
});
|
|
6924
|
+
|
|
6925
|
+
if (!response.ok) {
|
|
6926
|
+
throw new Error(\`Failed to update ${camelName}: \${response.statusText}\`);
|
|
6927
|
+
}
|
|
6928
|
+
|
|
6929
|
+
return response.json();
|
|
6930
|
+
}
|
|
6931
|
+
|
|
6932
|
+
/**
|
|
6933
|
+
* Delete a ${camelName}
|
|
6934
|
+
*/
|
|
6935
|
+
async delete(id: string): Promise<Delete${pascalName}Response> {
|
|
6936
|
+
const response = await fetch(\`\${this.baseUrl}/${name}/\${id}\`, {
|
|
6937
|
+
method: 'DELETE',
|
|
6938
|
+
headers: this.headers,
|
|
6939
|
+
});
|
|
6940
|
+
|
|
6941
|
+
if (!response.ok) {
|
|
6942
|
+
throw new Error(\`Failed to delete ${camelName}: \${response.statusText}\`);
|
|
6943
|
+
}
|
|
6944
|
+
|
|
6945
|
+
return response.json();
|
|
6946
|
+
}
|
|
6947
|
+
|
|
6948
|
+
/**
|
|
6949
|
+
* List all ${name}
|
|
6950
|
+
*/
|
|
6951
|
+
async list(): Promise<List${pascalName}sResponse> {
|
|
6952
|
+
const response = await fetch(\`\${this.baseUrl}/${name}\`, {
|
|
6953
|
+
method: 'GET',
|
|
6954
|
+
headers: this.headers,
|
|
6955
|
+
});
|
|
6956
|
+
|
|
6957
|
+
if (!response.ok) {
|
|
6958
|
+
throw new Error(\`Failed to list ${name}: \${response.statusText}\`);
|
|
6959
|
+
}
|
|
6960
|
+
|
|
6961
|
+
return response.json();
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
`;
|
|
6965
|
+
}
|
|
6966
|
+
|
|
6967
|
+
function generateTest(name) {
|
|
6968
|
+
const pascalName = toPascalCase(name);
|
|
6969
|
+
const camelName = toCamelCase(name);
|
|
6970
|
+
return `import { describe, it, expect, beforeAll } from 'vitest';
|
|
6971
|
+
import { ${pascalName}Client } from '../src/sdk/${name}Client';
|
|
6972
|
+
|
|
6973
|
+
// CONFIG: Change this to your running API URL
|
|
6974
|
+
const BASE_URL = process.env.API_URL || 'http://localhost:3000';
|
|
6975
|
+
|
|
6976
|
+
describe('${pascalName} Resource Integration Tests', () => {
|
|
6977
|
+
const client = new ${pascalName}Client(BASE_URL);
|
|
6978
|
+
let createdId: string;
|
|
6979
|
+
|
|
6980
|
+
beforeAll(async () => {
|
|
6981
|
+
// Optional: Check if server is running
|
|
6982
|
+
try {
|
|
6983
|
+
await fetch(BASE_URL);
|
|
6984
|
+
} catch (e) {
|
|
6985
|
+
console.warn('ā ļø Warning: API server might not be running at ' + BASE_URL);
|
|
6986
|
+
console.warn(' Tests might fail if the server is not reachable.');
|
|
6987
|
+
}
|
|
6988
|
+
});
|
|
6989
|
+
|
|
6990
|
+
it('should create a new ${camelName}', async () => {
|
|
6991
|
+
const data = {
|
|
6992
|
+
// TODO: Add required fields based on your contract
|
|
6993
|
+
name: 'Test ${pascalName}',
|
|
6994
|
+
// email: 'test@example.com',
|
|
6995
|
+
};
|
|
6996
|
+
|
|
6997
|
+
try {
|
|
6998
|
+
const response = await client.create(data as any);
|
|
6999
|
+
expect(response).toBeDefined();
|
|
7000
|
+
// Assuming the response has an id
|
|
7001
|
+
if ((response as any).id) {
|
|
7002
|
+
createdId = (response as any).id;
|
|
7003
|
+
}
|
|
7004
|
+
} catch (error) {
|
|
7005
|
+
// Ignore error if server is not implemented yet
|
|
7006
|
+
console.log('Skipping create test (server likely not implemented)');
|
|
7007
|
+
}
|
|
7008
|
+
});
|
|
7009
|
+
|
|
7010
|
+
it('should list all ${name}', async () => {
|
|
7011
|
+
try {
|
|
7012
|
+
const list = await client.list();
|
|
7013
|
+
expect(Array.isArray(list)).toBe(true);
|
|
7014
|
+
} catch (error) {
|
|
7015
|
+
console.log('Skipping list test');
|
|
7016
|
+
}
|
|
7017
|
+
});
|
|
7018
|
+
|
|
7019
|
+
it('should get a ${camelName} by ID', async () => {
|
|
7020
|
+
if (!createdId) return;
|
|
7021
|
+
|
|
7022
|
+
try {
|
|
7023
|
+
const item = await client.get(createdId);
|
|
7024
|
+
expect(item).toBeDefined();
|
|
7025
|
+
} catch (error) {
|
|
7026
|
+
console.log('Skipping get test');
|
|
7027
|
+
}
|
|
7028
|
+
});
|
|
7029
|
+
|
|
7030
|
+
it('should update a ${camelName}', async () => {
|
|
7031
|
+
if (!createdId) return;
|
|
7032
|
+
|
|
7033
|
+
const updateData = {
|
|
7034
|
+
name: 'Updated ${pascalName}',
|
|
7035
|
+
};
|
|
7036
|
+
|
|
7037
|
+
try {
|
|
7038
|
+
const updated = await client.update(createdId, updateData as any);
|
|
7039
|
+
expect(updated).toBeDefined();
|
|
7040
|
+
} catch (error) {
|
|
7041
|
+
console.log('Skipping update test');
|
|
7042
|
+
}
|
|
7043
|
+
});
|
|
7044
|
+
|
|
7045
|
+
it('should delete a ${camelName}', async () => {
|
|
7046
|
+
if (!createdId) return;
|
|
7047
|
+
|
|
7048
|
+
try {
|
|
7049
|
+
const result = await client.delete(createdId);
|
|
7050
|
+
expect(result).toBeDefined();
|
|
7051
|
+
} catch (error) {
|
|
7052
|
+
console.log('Skipping delete test');
|
|
7053
|
+
}
|
|
7054
|
+
});
|
|
7055
|
+
});
|
|
7056
|
+
`;
|
|
7057
|
+
}
|
|
7058
|
+
|
|
6857
7059
|
// Helper to load the main library
|
|
6858
7060
|
function loadSchemaApi() {
|
|
6859
7061
|
try {
|
|
@@ -6865,7 +7067,7 @@ function loadSchemaApi() {
|
|
|
6865
7067
|
return {};
|
|
6866
7068
|
}
|
|
6867
7069
|
}
|
|
6868
|
-
function loadContractsModule
|
|
7070
|
+
function loadContractsModule(contractsDir) {
|
|
6869
7071
|
const cwd = process.cwd();
|
|
6870
7072
|
const baseDir = path__namespace.join(cwd, contractsDir || "contracts");
|
|
6871
7073
|
const candidates = ["index.js", "index.cjs", "index.ts"];
|
|
@@ -6925,7 +7127,7 @@ function generateDocs(config) {
|
|
|
6925
7127
|
if (adapter || contractsDir) {
|
|
6926
7128
|
console.log(`Using config: adapter=${adapter || "unknown"}, contractsDir=${contractsDir}`);
|
|
6927
7129
|
}
|
|
6928
|
-
const contractsModule = loadContractsModule
|
|
7130
|
+
const contractsModule = loadContractsModule(contractsDir);
|
|
6929
7131
|
if (!contractsModule) {
|
|
6930
7132
|
console.error(`Could not find contracts entry in ${contractsDir}. Expected index.js or index.cjs`);
|
|
6931
7133
|
return;
|
|
@@ -6979,19 +7181,36 @@ function handleGenerate(args) {
|
|
|
6979
7181
|
return;
|
|
6980
7182
|
}
|
|
6981
7183
|
if (type === 'sdk') {
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
console.log(`Using config: adapter=${adapter || "unknown"}, contractsDir=${config?.contractsDir || "contracts"}`);
|
|
7184
|
+
if (!name) {
|
|
7185
|
+
console.error('Usage: schemaapi generate sdk <name>');
|
|
7186
|
+
return;
|
|
6986
7187
|
}
|
|
7188
|
+
console.log(`Generating SDK Client for "${name}"...`);
|
|
7189
|
+
const cwd = process.cwd();
|
|
7190
|
+
const sdkDir = path__namespace.join(cwd, 'src', 'sdk');
|
|
7191
|
+
ensureDir(sdkDir);
|
|
7192
|
+
const sdkContent = generateSdkClient(name);
|
|
7193
|
+
writeFile(path__namespace.join(sdkDir, `${name}Client.ts`), sdkContent);
|
|
7194
|
+
console.log(`Created SDK Client in src/sdk/${name}Client.ts`);
|
|
7195
|
+
console.log(`\nUsage example:`);
|
|
7196
|
+
console.log(`import { ${name.charAt(0).toUpperCase() + name.slice(1)}Client } from './sdk/${name}Client';`);
|
|
7197
|
+
console.log(`const client = new ${name.charAt(0).toUpperCase() + name.slice(1)}Client('http://localhost:3000');`);
|
|
6987
7198
|
return;
|
|
6988
7199
|
}
|
|
6989
7200
|
if (type === 'tests') {
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
console.log(`Using config: adapter=${adapter || "unknown"}, contractsDir=${config?.contractsDir || "contracts"}`);
|
|
7201
|
+
if (!name) {
|
|
7202
|
+
console.error('Usage: schemaapi generate tests <name>');
|
|
7203
|
+
return;
|
|
6994
7204
|
}
|
|
7205
|
+
console.log(`Generating Integration Tests for "${name}"...`);
|
|
7206
|
+
const cwd = process.cwd();
|
|
7207
|
+
const testsDir = path__namespace.join(cwd, 'tests');
|
|
7208
|
+
ensureDir(testsDir);
|
|
7209
|
+
const testContent = generateTest(name);
|
|
7210
|
+
writeFile(path__namespace.join(testsDir, `${name}.test.ts`), testContent);
|
|
7211
|
+
console.log(`Created Integration Test in tests/${name}.test.ts`);
|
|
7212
|
+
console.log(`\nRequires 'vitest' and the generated SDK.`);
|
|
7213
|
+
console.log(`Run with: npx vitest tests/${name}.test.ts`);
|
|
6995
7214
|
return;
|
|
6996
7215
|
}
|
|
6997
7216
|
if (type !== 'resource') {
|
|
@@ -7099,100 +7318,6 @@ function handleGenerate(args) {
|
|
|
7099
7318
|
}
|
|
7100
7319
|
}
|
|
7101
7320
|
|
|
7102
|
-
function loadContractsModule(contractsDir) {
|
|
7103
|
-
const cwd = process.cwd();
|
|
7104
|
-
const baseDir = path__namespace.join(cwd, contractsDir);
|
|
7105
|
-
const candidates = ["index.js", "index.cjs", "index.ts"];
|
|
7106
|
-
for (const file of candidates) {
|
|
7107
|
-
const fullPath = path__namespace.join(baseDir, file);
|
|
7108
|
-
if (fs__namespace.existsSync(fullPath)) {
|
|
7109
|
-
try {
|
|
7110
|
-
// We use require/import to load the contract objects
|
|
7111
|
-
// In a real TS environment, we might need ts-node/register if loading .ts directly
|
|
7112
|
-
// But assuming the user has compiled code or we are running in a compatible env:
|
|
7113
|
-
if (file.endsWith('.ts')) {
|
|
7114
|
-
try {
|
|
7115
|
-
// Register ts-node with specific options to handle imports correctly
|
|
7116
|
-
require('ts-node').register({
|
|
7117
|
-
transpileOnly: true,
|
|
7118
|
-
compilerOptions: {
|
|
7119
|
-
module: 'commonjs',
|
|
7120
|
-
esModuleInterop: true
|
|
7121
|
-
}
|
|
7122
|
-
});
|
|
7123
|
-
}
|
|
7124
|
-
catch (e) {
|
|
7125
|
-
// Ignore if ts-node is not available
|
|
7126
|
-
}
|
|
7127
|
-
}
|
|
7128
|
-
return require(fullPath);
|
|
7129
|
-
}
|
|
7130
|
-
catch (error) {
|
|
7131
|
-
console.error(`Failed to load contracts module at ${fullPath}:`, (error && error.message) || error);
|
|
7132
|
-
return null;
|
|
7133
|
-
}
|
|
7134
|
-
}
|
|
7135
|
-
}
|
|
7136
|
-
return null;
|
|
7137
|
-
}
|
|
7138
|
-
function handleAudit(args) {
|
|
7139
|
-
const config = loadConfig();
|
|
7140
|
-
const contractsDir = config?.contractsDir || "contracts";
|
|
7141
|
-
console.log(`š Starting SchemaApi Audit in directory: ${contractsDir}`);
|
|
7142
|
-
const contractsModule = loadContractsModule(contractsDir);
|
|
7143
|
-
if (!contractsModule) {
|
|
7144
|
-
console.error(`ā Could not find contracts entry in ${contractsDir}. Make sure index.ts/js exists and exports your contracts.`);
|
|
7145
|
-
return;
|
|
7146
|
-
}
|
|
7147
|
-
let totalIssues = 0;
|
|
7148
|
-
const candidates = Object.values(contractsModule).filter((value) => value &&
|
|
7149
|
-
typeof value === "object" &&
|
|
7150
|
-
typeof value.docs === "function" // Duck typing for Contract
|
|
7151
|
-
);
|
|
7152
|
-
if (candidates.length === 0) {
|
|
7153
|
-
console.warn("ā ļø No contracts found exported in the entry file.");
|
|
7154
|
-
return;
|
|
7155
|
-
}
|
|
7156
|
-
candidates.forEach((contract, index) => {
|
|
7157
|
-
const docs = contract.docs(); // This returns the contract structure
|
|
7158
|
-
const routes = docs.routes || [];
|
|
7159
|
-
console.log(`\nš Contract #${index + 1}: Found ${routes.length} routes`);
|
|
7160
|
-
routes.forEach((route) => {
|
|
7161
|
-
const issues = [];
|
|
7162
|
-
const method = route.method;
|
|
7163
|
-
const pathStr = route.path;
|
|
7164
|
-
const fullRoute = `${method} ${pathStr}`;
|
|
7165
|
-
// 1. Security Check: Roles
|
|
7166
|
-
if (!route.roles || route.roles.length === 0) {
|
|
7167
|
-
issues.push(`ā ļø [Security] No roles defined. Endpoint is public? Consider adding explicit roles or 'public'.`);
|
|
7168
|
-
}
|
|
7169
|
-
// 2. Validation Check: Params
|
|
7170
|
-
// Basic check: if path has :param, schema should have it
|
|
7171
|
-
const pathParams = (pathStr.match(/:[a-zA-Z0-9_]+/g) || []).map((p) => p.substring(1));
|
|
7172
|
-
if (pathParams.length > 0) ;
|
|
7173
|
-
// 3. Completeness: Errors
|
|
7174
|
-
if (!route.errors || Object.keys(route.errors).length === 0) {
|
|
7175
|
-
issues.push(`ā¹ļø [Best Practice] No error status codes defined. Clients won't know what errors to expect.`);
|
|
7176
|
-
}
|
|
7177
|
-
if (issues.length > 0) {
|
|
7178
|
-
console.log(` šø ${fullRoute}`);
|
|
7179
|
-
issues.forEach(issue => console.log(` ${issue}`));
|
|
7180
|
-
totalIssues += issues.length;
|
|
7181
|
-
}
|
|
7182
|
-
else {
|
|
7183
|
-
console.log(` ā
${fullRoute}`);
|
|
7184
|
-
}
|
|
7185
|
-
});
|
|
7186
|
-
});
|
|
7187
|
-
console.log(`\nš Audit complete.`);
|
|
7188
|
-
if (totalIssues > 0) {
|
|
7189
|
-
console.log(`Found ${totalIssues} potential issues.`);
|
|
7190
|
-
}
|
|
7191
|
-
else {
|
|
7192
|
-
console.log(`No obvious issues found. Great job! š`);
|
|
7193
|
-
}
|
|
7194
|
-
}
|
|
7195
|
-
|
|
7196
7321
|
async function cli(args) {
|
|
7197
7322
|
const command = args[0];
|
|
7198
7323
|
switch (command) {
|
|
@@ -7202,8 +7327,21 @@ async function cli(args) {
|
|
|
7202
7327
|
case 'generate':
|
|
7203
7328
|
handleGenerate(args.slice(1));
|
|
7204
7329
|
break;
|
|
7205
|
-
case '
|
|
7206
|
-
|
|
7330
|
+
case 'help':
|
|
7331
|
+
case '--help':
|
|
7332
|
+
case '-h':
|
|
7333
|
+
console.log(`
|
|
7334
|
+
@codexsploitx/schemaapi CLI
|
|
7335
|
+
|
|
7336
|
+
Usage:
|
|
7337
|
+
npx @codexsploitx/schemaapi init <framework> Initialize configuration
|
|
7338
|
+
npx @codexsploitx/schemaapi generate resource <name> Generate Backend Resource (API Routes)
|
|
7339
|
+
npx @codexsploitx/schemaapi generate docs Generate documentation
|
|
7340
|
+
npx @codexsploitx/schemaapi generate sdk <name> Generate Frontend Client (SDK)
|
|
7341
|
+
npx @codexsploitx/schemaapi generate tests <name> Generate Integration Tests
|
|
7342
|
+
npx @codexsploitx/schemaapi adapters List available adapters
|
|
7343
|
+
npx @codexsploitx/schemaapi help Show this help message
|
|
7344
|
+
`);
|
|
7207
7345
|
break;
|
|
7208
7346
|
case 'adapters':
|
|
7209
7347
|
console.log(`
|
|
@@ -7221,16 +7359,9 @@ Available Adapters:
|
|
|
7221
7359
|
break;
|
|
7222
7360
|
default:
|
|
7223
7361
|
console.log(`
|
|
7224
|
-
|
|
7362
|
+
Unknown command: ${command}
|
|
7225
7363
|
|
|
7226
|
-
|
|
7227
|
-
npx schemaapi init <framework> Initialize configuration
|
|
7228
|
-
npx schemaapi generate resource <name> Generate a new resource
|
|
7229
|
-
npx schemaapi generate docs Generate documentation
|
|
7230
|
-
npx schemaapi generate sdk Generate SDK (coming soon)
|
|
7231
|
-
npx schemaapi generate tests Generate tests (coming soon)
|
|
7232
|
-
npx schemaapi audit Audit contracts
|
|
7233
|
-
npx schemaapi adapters List available adapters
|
|
7364
|
+
Run 'npx @codexsploitx/schemaapi help' to see available commands.
|
|
7234
7365
|
`);
|
|
7235
7366
|
}
|
|
7236
7367
|
}
|