@major-tech/resource-client 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ pnpm install @major-tech/resource-client
18
18
 
19
19
  **PostgreSQL:**
20
20
  ```typescript
21
- import { PostgresResourceClient } from '@major/resource-client';
21
+ import { PostgresResourceClient } from '@major-tech/resource-client';
22
22
  const client = new PostgresResourceClient({ baseUrl, applicationId, resourceId, majorJwtToken? });
23
23
  const res = await client.invoke('SELECT * FROM users WHERE id = $1', [123], 'fetch-user-by-id');
24
24
  // res.ok ? res.result.rows / res.result.rowsAffected : res.error.message
@@ -26,7 +26,7 @@ const res = await client.invoke('SELECT * FROM users WHERE id = $1', [123], 'fet
26
26
 
27
27
  **Custom API:**
28
28
  ```typescript
29
- import { CustomApiResourceClient } from '@major/resource-client';
29
+ import { CustomApiResourceClient } from '@major-tech/resource-client';
30
30
  const client = new CustomApiResourceClient({ baseUrl, applicationId, resourceId });
31
31
  const res = await client.invoke('POST', '/v1/payments', 'create-payment', {
32
32
  query: { currency: 'USD' }, headers: { 'X-Custom': 'value' },
@@ -37,7 +37,7 @@ const res = await client.invoke('POST', '/v1/payments', 'create-payment', {
37
37
 
38
38
  **HubSpot:**
39
39
  ```typescript
40
- import { HubSpotResourceClient } from '@major/resource-client';
40
+ import { HubSpotResourceClient } from '@major-tech/resource-client';
41
41
  const client = new HubSpotResourceClient({ baseUrl, applicationId, resourceId });
42
42
  const res = await client.invoke('GET', '/crm/v3/objects/contacts', 'fetch-contacts', { query: { limit: '10' } });
43
43
  // res.ok && res.result.body.kind === 'json' ? res.result.body.value : res.error
@@ -45,7 +45,7 @@ const res = await client.invoke('GET', '/crm/v3/objects/contacts', 'fetch-contac
45
45
 
46
46
  **S3:**
47
47
  ```typescript
48
- import { S3ResourceClient } from '@major/resource-client';
48
+ import { S3ResourceClient } from '@major-tech/resource-client';
49
49
  const client = new S3ResourceClient({ baseUrl, applicationId, resourceId });
50
50
  const res = await client.invoke('ListObjectsV2', { Bucket: 'my-bucket', Prefix: 'uploads/' }, 'list-uploads');
51
51
  // res.ok ? res.result.data : res.error
@@ -55,7 +55,7 @@ const url = await client.invoke('GeneratePresignedUrl', { Bucket: 'my-bucket', K
55
55
 
56
56
  **Error Handling:**
57
57
  ```typescript
58
- import { ResourceInvokeError } from '@major/resource-client';
58
+ import { ResourceInvokeError } from '@major-tech/resource-client';
59
59
  try { await client.invoke(...); }
60
60
  catch (e) { if (e instanceof ResourceInvokeError) { e.message, e.httpStatus, e.requestId } }
61
61
  ```
@@ -64,4 +64,27 @@ catch (e) { if (e instanceof ResourceInvokeError) { e.message, e.httpStatus, e.r
64
64
 
65
65
  **Types:** All exported: `BaseClientConfig`, `DatabaseInvokeResponse`, `ApiInvokeResponse`, `StorageInvokeResponse`, `HttpMethod`, `QueryParams`, `BodyPayload`, `S3Command`, etc.
66
66
 
67
+ ## 🛠️ CLI Tool - Generate Singleton Clients
68
+
69
+ The package includes a CLI tool to generate pre-configured singleton clients for your resources:
70
+
71
+ ```bash
72
+ # Add a resource
73
+ npx major-client add "resource-123" "orders-db" "database-postgresql" "Orders database" "app-456"
74
+
75
+ # List all resources
76
+ npx major-client list
77
+
78
+ # Remove a resource
79
+ npx major-client remove "orders-db"
80
+ ```
81
+
82
+ This generates TypeScript files in `src/clients/` that you can import:
83
+ ```typescript
84
+ import { ordersDbClient } from './clients';
85
+ const result = await ordersDbClient.invoke('SELECT * FROM orders', [], 'list-orders');
86
+ ```
87
+
88
+ **Types:** `database-postgresql` | `api-custom` | `api-hubspot` | `storage-s3`
89
+
67
90
  MIT License
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool to generate singleton resource clients
4
+ * Part of @major-tech/resource-client
5
+ *
6
+ * Usage:
7
+ * npx @major-tech/resource-client add <resource_id> <name> <type> <description> <application_id>
8
+ * npx @major-tech/resource-client remove <name>
9
+ * npx @major-tech/resource-client list
10
+ *
11
+ * Types: database-postgresql | api-hubspot | api-custom | storage-s3
12
+ *
13
+ * Examples:
14
+ * npx @major-tech/resource-client add "abc-123" "orders-db" "database-postgresql" "Orders database" "app-123"
15
+ * npx @major-tech/resource-client add "xyz-789" "payment-api" "api-custom" "Payment API" "app-456"
16
+ * npx @major-tech/resource-client remove "orders-db"
17
+ * npx @major-tech/resource-client list
18
+ */
19
+
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import { fileURLToPath } from 'url';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+
27
+ // User's project root (where the command is run from)
28
+ const projectRoot = process.cwd();
29
+ const resourcesConfigPath = path.join(projectRoot, 'resources.json');
30
+ const clientsDir = path.join(projectRoot, 'src', 'clients');
31
+
32
+ /**
33
+ * Client template
34
+ */
35
+ function clientTemplate(data) {
36
+ return `import { ${data.clientClass} } from '@major-tech/resource-client';
37
+
38
+ /**
39
+ * ${data.description}
40
+ *
41
+ * Type: ${data.type}
42
+ * Resource ID: ${data.resourceId}
43
+ * Application ID: ${data.applicationId}
44
+ *
45
+ * DO NOT EDIT - Auto-generated by @major-tech/resource-client
46
+ */
47
+
48
+ const BASE_URL = import.meta.env.VITE_MAJOR_API_BASE_URL || 'https://api.major.tech';
49
+ const MAJOR_JWT_TOKEN = import.meta.env.VITE_MAJOR_JWT_TOKEN;
50
+
51
+ class ${data.clientName}Singleton {
52
+ private static instance: ${data.clientClass} | null = null;
53
+
54
+ static getInstance(): ${data.clientClass} {
55
+ if (!${data.clientName}Singleton.instance) {
56
+ ${data.clientName}Singleton.instance = new ${data.clientClass}({
57
+ baseUrl: BASE_URL,
58
+ majorJwtToken: MAJOR_JWT_TOKEN,
59
+ applicationId: '${data.applicationId}',
60
+ resourceId: '${data.resourceId}',
61
+ });
62
+ }
63
+ return ${data.clientName}Singleton.instance;
64
+ }
65
+ }
66
+
67
+ export const ${data.clientName} = ${data.clientName}Singleton.getInstance();
68
+ `;
69
+ }
70
+
71
+ /**
72
+ * Index template
73
+ */
74
+ function indexTemplate(data) {
75
+ return `/**
76
+ * Auto-generated client exports
77
+ * DO NOT EDIT - Generated by @major-tech/resource-client
78
+ */
79
+
80
+ ${data.exports.join('\n')}
81
+ `;
82
+ }
83
+
84
+ /**
85
+ * Load resources from JSON config file
86
+ */
87
+ function loadResources() {
88
+ if (!fs.existsSync(resourcesConfigPath)) {
89
+ return [];
90
+ }
91
+ const content = fs.readFileSync(resourcesConfigPath, 'utf-8');
92
+ return JSON.parse(content);
93
+ }
94
+
95
+ /**
96
+ * Save resources to JSON config file
97
+ */
98
+ function saveResources(resources) {
99
+ const dir = path.dirname(resourcesConfigPath);
100
+ if (!fs.existsSync(dir)) {
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ }
103
+ fs.writeFileSync(resourcesConfigPath, JSON.stringify(resources, null, 2), 'utf-8');
104
+ }
105
+
106
+ /**
107
+ * Convert a resource name to a valid camelCase identifier
108
+ * e.g., "yo mama" -> "yoMama", "orders-db" -> "ordersDb"
109
+ */
110
+ function toCamelCase(str) {
111
+ return str
112
+ .toLowerCase()
113
+ .replace(/[^a-z0-9]+(.)/g, (_, char) => char.toUpperCase())
114
+ .replace(/^[^a-z]+/, ''); // Remove leading non-letters
115
+ }
116
+
117
+ function getClientClass(type) {
118
+ const typeMap = {
119
+ 'database-postgresql': 'PostgresResourceClient',
120
+ 'api-custom': 'CustomApiResourceClient',
121
+ 'api-hubspot': 'HubSpotResourceClient',
122
+ 'storage-s3': 'S3ResourceClient',
123
+ };
124
+ return typeMap[type] || 'PostgresResourceClient';
125
+ }
126
+
127
+ function generateClientFile(resource) {
128
+ const clientName = toCamelCase(resource.name) + 'Client';
129
+ const clientClass = getClientClass(resource.type);
130
+
131
+ return clientTemplate({
132
+ clientClass,
133
+ description: resource.description,
134
+ type: resource.type,
135
+ resourceId: resource.id,
136
+ applicationId: resource.applicationId,
137
+ clientName,
138
+ });
139
+ }
140
+
141
+ function generateIndexFile(resources) {
142
+ if (resources.length === 0) {
143
+ return `// No clients configured\n`;
144
+ }
145
+
146
+ const exports = resources.map(resource => {
147
+ const clientName = toCamelCase(resource.name) + 'Client';
148
+ const fileName = toCamelCase(resource.name);
149
+ return `export { ${clientName} } from './${fileName}';`;
150
+ });
151
+
152
+ return indexTemplate({ exports });
153
+ }
154
+
155
+ function addResource(resourceId, name, type, description, applicationId) {
156
+ const validTypes = ['database-postgresql', 'api-hubspot', 'api-custom', 'storage-s3'];
157
+ if (!validTypes.includes(type)) {
158
+ console.error(`❌ Invalid type: ${type}`);
159
+ console.log(` Valid types: ${validTypes.join(', ')}`);
160
+ process.exit(1);
161
+ }
162
+
163
+ if (!applicationId) {
164
+ console.error('❌ Application ID is required');
165
+ process.exit(1);
166
+ }
167
+
168
+ const resources = loadResources();
169
+
170
+ const existing = resources.find(r => r.name === name);
171
+ if (existing) {
172
+ console.error(`❌ Resource with name "${name}" already exists`);
173
+ process.exit(1);
174
+ }
175
+
176
+ const newResource = {
177
+ id: resourceId,
178
+ name,
179
+ type,
180
+ description,
181
+ applicationId
182
+ };
183
+
184
+ resources.push(newResource);
185
+ saveResources(resources);
186
+
187
+ console.log(`✅ Added resource: ${name}`);
188
+ console.log(` Type: ${type}`);
189
+ console.log(` ID: ${resourceId}`);
190
+
191
+ regenerateClients(resources);
192
+ }
193
+
194
+ /**
195
+ * Remove a resource by name
196
+ */
197
+ function removeResource(name) {
198
+ const resources = loadResources();
199
+ const index = resources.findIndex(r => r.name === name);
200
+
201
+ if (index === -1) {
202
+ console.error(`❌ Resource "${name}" not found`);
203
+ process.exit(1);
204
+ }
205
+
206
+ const removed = resources.splice(index, 1)[0];
207
+ saveResources(resources);
208
+
209
+ console.log(`✅ Removed resource: ${removed.name}`);
210
+ console.log(` ID: ${removed.id}`);
211
+
212
+ regenerateClients(resources);
213
+ }
214
+
215
+ /**
216
+ * List all resources
217
+ */
218
+ function listResources() {
219
+ const resources = loadResources();
220
+
221
+ if (resources.length === 0) {
222
+ console.log('No resources configured');
223
+ return;
224
+ }
225
+
226
+ console.log(`Resources (${resources.length}):\n`);
227
+ resources.forEach((r, idx) => {
228
+ console.log(`${idx + 1}. ${r.name} (${r.type})`);
229
+ console.log(` ID: ${r.id}`);
230
+ console.log(` Client: ${toCamelCase(r.name)}Client`);
231
+ console.log('');
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Regenerate all client files
237
+ */
238
+ function regenerateClients(resources) {
239
+ // Ensure clients directory exists
240
+ if (!fs.existsSync(clientsDir)) {
241
+ fs.mkdirSync(clientsDir, { recursive: true });
242
+ }
243
+
244
+ // Clear existing client files (except index.ts temporarily)
245
+ const existingFiles = fs.readdirSync(clientsDir).filter(f => f !== 'index.ts');
246
+ existingFiles.forEach(file => {
247
+ fs.unlinkSync(path.join(clientsDir, file));
248
+ });
249
+
250
+ // Generate individual client files
251
+ resources.forEach(resource => {
252
+ const fileName = toCamelCase(resource.name) + '.ts';
253
+ const filePath = path.join(clientsDir, fileName);
254
+ const code = generateClientFile(resource);
255
+ fs.writeFileSync(filePath, code, 'utf-8');
256
+ });
257
+
258
+ // Generate index file
259
+ const indexPath = path.join(clientsDir, 'index.ts');
260
+ const indexCode = generateIndexFile(resources);
261
+ fs.writeFileSync(indexPath, indexCode, 'utf-8');
262
+
263
+ console.log(`✅ Generated ${resources.length} client(s) in ${clientsDir}`);
264
+ }
265
+
266
+ /**
267
+ * Main execution
268
+ */
269
+ function main() {
270
+ const args = process.argv.slice(2);
271
+ const command = args[0];
272
+
273
+ if (!command) {
274
+ console.log('Usage:');
275
+ console.log(' npx @major-tech/resource-client add <resource_id> <name> <type> <description> <application_id>');
276
+ console.log(' npx @major-tech/resource-client remove <name>');
277
+ console.log(' npx @major-tech/resource-client list');
278
+ console.log('\nTypes: database-postgresql | api-hubspot | api-custom | storage-s3');
279
+ return;
280
+ }
281
+
282
+ switch (command) {
283
+ case 'add': {
284
+ const [, resourceId, name, type, description, applicationId] = args;
285
+ if (!resourceId || !name || !type || !description || !applicationId) {
286
+ console.error('❌ Missing arguments');
287
+ console.log('Usage: add <resource_id> <name> <type> <description> <application_id>');
288
+ process.exit(1);
289
+ }
290
+ addResource(resourceId, name, type, description, applicationId);
291
+ break;
292
+ }
293
+
294
+ case 'remove': {
295
+ const [, name] = args;
296
+ if (!name) {
297
+ console.error('❌ Missing name');
298
+ process.exit(1);
299
+ }
300
+ removeResource(name);
301
+ break;
302
+ }
303
+
304
+ case 'list': {
305
+ listResources();
306
+ break;
307
+ }
308
+
309
+ case 'regenerate': {
310
+ const resources = loadResources();
311
+ regenerateClients(resources);
312
+ break;
313
+ }
314
+
315
+ default: {
316
+ console.error(`❌ Unknown command: ${command}`);
317
+ process.exit(1);
318
+ }
319
+ }
320
+ }
321
+
322
+ main();
323
+
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "@major-tech/resource-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "TypeScript client library for invoking Major resources (PostgreSQL, Custom APIs, HubSpot, S3)",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "engines": {
8
8
  "node": ">=18.0.0"
9
9
  },
10
+ "bin": {
11
+ "major-client": "./bin/generate-clients.mjs"
12
+ },
10
13
  "files": [
11
14
  "dist",
15
+ "bin",
12
16
  "README.md",
13
17
  "LICENSE"
14
18
  ],