@nextplus/js-sdk 0.0.10 → 0.0.12

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/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@nextplus/js-sdk",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "generate": "npm run clean && npm run prepare-spec && npm run openapi-ts && npm run build",
7
- "build": "tsup src/index.ts src/sdk/index.ts src/sdk/client/index.ts src/sdk/sdk.gen.ts src/smoke.ts --dts --format esm,cjs --outDir dist --clean",
7
+ "build": "tsup",
8
8
  "clean": "rm -rf dist && rm -rf .generated",
9
- "prepublishOnly": "npm run clean && tsup src/index.ts src/sdk/index.ts src/sdk/client/index.ts src/sdk/sdk.gen.ts --dts --format esm,cjs --outDir dist --clean",
9
+ "prepublishOnly": "npm run clean && SKIP_SMOKE=1 tsup",
10
10
  "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11
11
  "openapi-ts": "openapi-ts",
12
12
  "prepare-spec": "node scripts/prepare-spec.js",
13
- "test": "npm run build && node dist/smoke.js"
13
+ "test": "npm run build && node --test dist/smoke.js",
14
+ "test:watch": "tsx watch --test src/smoke.ts",
15
+ "test:dev": "tsx --test src/smoke.ts"
14
16
  },
15
17
  "files": [
16
18
  "dist",
@@ -24,6 +26,11 @@
24
26
  "import": "./dist/index.js",
25
27
  "require": "./dist/index.cjs"
26
28
  },
29
+ "./services": {
30
+ "types": "./dist/sdk/sdk.gen.d.ts",
31
+ "import": "./dist/sdk/sdk.gen.js",
32
+ "require": "./dist/sdk/sdk.gen.cjs"
33
+ },
27
34
  "./sdk": {
28
35
  "types": "./dist/sdk/index.d.ts",
29
36
  "import": "./dist/sdk/index.js",
@@ -52,6 +59,7 @@
52
59
  "eslint": "^9.32.0",
53
60
  "prettier": "^3.6.2",
54
61
  "tsup": "^8.5.0",
62
+ "tsx": "^4.21.0",
55
63
  "typescript": "^5.9.2"
56
64
  },
57
65
  "dependencies": {
package/src/index.ts CHANGED
@@ -1,18 +1,43 @@
1
1
  import debug from 'debug';
2
- import { createClient } from './sdk/client';
2
+ import { createClient, createConfig } from './sdk/client';
3
3
  import { UserModelService } from './sdk';
4
4
  import * as AllServices from './sdk';
5
5
  import type * as AllServiceTypes from './sdk';
6
6
 
7
7
  const log = debug('nextplus:sdk');
8
8
 
9
+ // Configuration types for token-based authentication
10
+ export type TokenConfig = {
11
+ baseURL?: string;
12
+ accessToken: string;
13
+ returnType?: 'raw' | 'data';
14
+ throwOnError?: boolean; // For tree-shakeable imports
15
+ };
16
+
17
+ // Configuration types for credential-based authentication
18
+ export type CredentialConfig = {
19
+ baseURL?: string;
20
+ email?: string;
21
+ username?: string;
22
+ password: string;
23
+ returnType?: 'raw' | 'data';
24
+ throwOnError?: boolean; // For tree-shakeable imports
25
+ };
26
+
27
+ // Union type for SDK configuration
28
+ export type SDKConfig = TokenConfig | CredentialConfig;
29
+
9
30
  const accessToken = {
10
31
  token: null as string | null,
11
32
  created: null as Date | null,
12
33
  ttl: 0,
13
34
  };
14
35
 
15
- const setAccessToken = (token: string, created: Date = new Date(), ttl: number = 1209600) => {
36
+ const setAccessToken = (
37
+ token: string,
38
+ created: Date = new Date(),
39
+ ttl: number = 1209600,
40
+ ) => {
16
41
  accessToken.token = token;
17
42
  accessToken.created = created;
18
43
  accessToken.ttl = ttl;
@@ -34,17 +59,18 @@ const isTokenValid = (): boolean => {
34
59
  return tokenAge < tokenTtlMs;
35
60
  };
36
61
 
37
-
38
-
39
62
  export class NextPlusSDK {
40
- private client: any;
41
- private credentials: { email?: string; username?: string; password: string };
63
+ public client: any; // Exposed for tree-shakeable service imports
64
+ private credentials?: { email?: string; username?: string; password: string };
65
+ private providedAccessToken?: string;
42
66
  private returnType: 'raw' | 'data' = 'data';
67
+ private authMode: 'token' | 'credentials';
43
68
 
44
69
  // Wrap plain developer inputs into the right container (body/query)
45
70
  // and stringify complex query params when required by LoopBack (filter/where)
46
71
  private normalizeOptions(methodName: string, rawOptions: any): any {
47
- const options: any = rawOptions && typeof rawOptions === 'object' ? { ...rawOptions } : {};
72
+ const options: any =
73
+ rawOptions && typeof rawOptions === 'object' ? { ...rawOptions } : {};
48
74
 
49
75
  // Always ensure client is set (will be set by caller afterwards if missing)
50
76
  // We avoid mutating the original object to keep predictable behavior
@@ -54,10 +80,22 @@ export class NextPlusSDK {
54
80
  const method = methodName.toString().toLowerCase();
55
81
  const isFind = method.includes('find') && !method.includes('count');
56
82
  const isCount = method.includes('count');
57
- const shouldUseBody = /(create|update|replace|patch|login|upsert)/i.test(methodName.toString());
83
+ const shouldUseBody = /(create|update|replace|patch|login|upsert)/i.test(
84
+ methodName.toString(),
85
+ );
58
86
 
59
- const topLevelKeys = ['filter', 'where', 'fields', 'order', 'limit', 'offset', 'include'];
60
- const hasTopLevelFilterBits = topLevelKeys.some((k) => Object.prototype.hasOwnProperty.call(options, k));
87
+ const topLevelKeys = [
88
+ 'filter',
89
+ 'where',
90
+ 'fields',
91
+ 'order',
92
+ 'limit',
93
+ 'offset',
94
+ 'include',
95
+ ];
96
+ const hasTopLevelFilterBits = topLevelKeys.some((k) =>
97
+ Object.prototype.hasOwnProperty.call(options, k),
98
+ );
61
99
 
62
100
  // Only create query if relevant for this method
63
101
  if (isFind) {
@@ -73,9 +111,13 @@ export class NextPlusSDK {
73
111
  if (options.order !== undefined) filterObject.order = options.order;
74
112
  if (options.limit !== undefined) filterObject.limit = options.limit;
75
113
  if (options.offset !== undefined) filterObject.offset = options.offset;
76
- if (options.include !== undefined) filterObject.include = options.include;
114
+ if (options.include !== undefined)
115
+ filterObject.include = options.include;
77
116
  if (Object.keys(filterObject).length > 0) {
78
- options.query.filter = typeof filterObject === 'string' ? filterObject : JSON.stringify(filterObject);
117
+ options.query.filter =
118
+ typeof filterObject === 'string'
119
+ ? filterObject
120
+ : JSON.stringify(filterObject);
79
121
  }
80
122
  // Clean moved keys from top-level
81
123
  topLevelKeys.forEach((k) => delete options[k]);
@@ -85,7 +127,8 @@ export class NextPlusSDK {
85
127
  // Allow top-level where or query.where as object → stringify
86
128
  if (options.where !== undefined) {
87
129
  const whereVal = options.where;
88
- options.query.where = typeof whereVal === 'string' ? whereVal : JSON.stringify(whereVal);
130
+ options.query.where =
131
+ typeof whereVal === 'string' ? whereVal : JSON.stringify(whereVal);
89
132
  delete options.where;
90
133
  }
91
134
  } else if (shouldUseBody) {
@@ -99,7 +142,8 @@ export class NextPlusSDK {
99
142
  options.body = bodyCandidate;
100
143
  // Keep only body/client/query/path at root
101
144
  Object.keys(options).forEach((k) => {
102
- if (!['body', 'client', 'query', 'path'].includes(k)) delete options[k];
145
+ if (!['body', 'client', 'query', 'path'].includes(k))
146
+ delete options[k];
103
147
  });
104
148
  }
105
149
  }
@@ -118,41 +162,83 @@ export class NextPlusSDK {
118
162
  return options;
119
163
  }
120
164
 
121
- constructor(options: {
122
- baseURL: string;
123
- email?: string;
124
- username?: string;
125
- password: string;
126
- returnType?: 'raw' | 'data';
127
- }) {
128
- // Validate that either email or username is provided
129
- if (!options.email && !options.username) {
130
- throw new Error('Either email or username must be provided');
165
+ constructor(options: SDKConfig) {
166
+ // Determine authentication mode
167
+ const hasToken = 'accessToken' in options && options.accessToken;
168
+ const hasPassword = 'password' in options && options.password;
169
+
170
+ // Validate mutually exclusive authentication
171
+ if (hasToken && hasPassword) {
172
+ throw new Error(
173
+ 'Provide either accessToken or credentials (email/username + password), not both',
174
+ );
131
175
  }
132
- if (options.email && options.username) {
133
- throw new Error('Provide either email or username, not both');
176
+
177
+ if (!hasToken && !hasPassword) {
178
+ throw new Error(
179
+ 'Either accessToken or credentials (email/username + password) must be provided',
180
+ );
181
+ }
182
+
183
+ // Set authentication mode
184
+ if (hasToken) {
185
+ this.authMode = 'token';
186
+ this.providedAccessToken = (options as TokenConfig).accessToken;
187
+ // Set the token immediately for token-based auth
188
+ setAccessToken(this.providedAccessToken, new Date(), 1209600);
189
+ } else {
190
+ this.authMode = 'credentials';
191
+ const credConfig = options as CredentialConfig;
192
+
193
+ // Validate that either email or username is provided
194
+ if (!credConfig.email && !credConfig.username) {
195
+ throw new Error(
196
+ 'Either email or username must be provided with password',
197
+ );
198
+ }
199
+ if (credConfig.email && credConfig.username) {
200
+ throw new Error('Provide either email or username, not both');
201
+ }
202
+
203
+ this.credentials = {
204
+ email: credConfig.email,
205
+ username: credConfig.username,
206
+ password: credConfig.password,
207
+ };
134
208
  }
135
209
 
136
- this.credentials = {
137
- email: options.email,
138
- username: options.username,
139
- password: options.password
140
- };
141
210
  this.returnType = options.returnType || 'data';
142
- // remove trailing slash from baseURL
143
- if (options.baseURL.endsWith('/')) {
144
- options.baseURL = options.baseURL.slice(0, -1);
211
+
212
+ // Handle baseURL with smart defaults
213
+ let baseURL = options.baseURL;
214
+ if (!baseURL) {
215
+ // Try to detect browser environment
216
+ if (typeof window !== 'undefined' && window.location) {
217
+ baseURL = window.location.origin + '/api';
218
+ } else {
219
+ throw new Error('baseURL is required in non-browser environments');
220
+ }
145
221
  }
146
- // if baseURL does not end with /api, add it
147
- if (!options.baseURL.endsWith('/api')) {
148
- options.baseURL = options.baseURL + '/api';
222
+
223
+ // Normalize baseURL
224
+ if (baseURL.endsWith('/')) {
225
+ baseURL = baseURL.slice(0, -1);
149
226
  }
150
- this.client = createClient({
151
- baseUrl: options.baseURL,
152
- });
227
+ if (!baseURL.endsWith('/api')) {
228
+ baseURL = baseURL + '/api';
229
+ }
230
+
231
+ this.client = createClient(
232
+ createConfig({
233
+ baseUrl: baseURL,
234
+ throwOnError: options.throwOnError ?? false,
235
+ }),
236
+ );
153
237
 
154
238
  // Add authentication interceptor
155
- this.client.interceptors.request.use(this.authRequestInterceptor.bind(this));
239
+ this.client.interceptors.request.use(
240
+ this.authRequestInterceptor.bind(this),
241
+ );
156
242
 
157
243
  // Create service proxies dynamically
158
244
  this.createServiceProxies();
@@ -164,24 +250,37 @@ export class NextPlusSDK {
164
250
  return request;
165
251
  }
166
252
 
167
- // Ensure we have a valid token
168
- await this.ensureValidToken();
169
-
170
- const token = getAccessToken();
171
- if (token.token) {
172
- request.headers.set('Authorization', token.token);
253
+ // Handle authentication based on mode
254
+ if (this.authMode === 'token') {
255
+ // For token mode, use the provided token directly
256
+ if (this.providedAccessToken) {
257
+ request.headers.set('Authorization', this.providedAccessToken);
258
+ }
259
+ } else {
260
+ // For credentials mode, ensure we have a valid token
261
+ await this.ensureValidToken();
262
+ const token = getAccessToken();
263
+ if (token.token) {
264
+ request.headers.set('Authorization', token.token);
265
+ }
173
266
  }
267
+
174
268
  return request;
175
269
  }
176
270
 
177
271
  private async ensureValidToken(): Promise<void> {
178
- if (!isTokenValid()) {
272
+ // Only validate token for credentials mode
273
+ if (this.authMode === 'credentials' && !isTokenValid()) {
179
274
  log('Token expired or missing, refreshing...');
180
275
  await this.login();
181
276
  }
182
277
  }
183
278
 
184
279
  private async login(): Promise<boolean> {
280
+ if (this.authMode !== 'credentials' || !this.credentials) {
281
+ throw new Error('Login is only available in credentials mode');
282
+ }
283
+
185
284
  try {
186
285
  const loginBody: any = {
187
286
  password: this.credentials.password,
@@ -219,11 +318,13 @@ export class NextPlusSDK {
219
318
 
220
319
  private createServiceProxies() {
221
320
  // Get all service classes from the SDK
222
- const serviceNames = Object.keys(AllServices).filter(key =>
223
- key.endsWith('Service') && typeof (AllServices as any)[key] === 'function'
321
+ const serviceNames = Object.keys(AllServices).filter(
322
+ (key) =>
323
+ key.endsWith('Service') &&
324
+ typeof (AllServices as any)[key] === 'function',
224
325
  );
225
326
 
226
- serviceNames.forEach(serviceName => {
327
+ serviceNames.forEach((serviceName) => {
227
328
  const ServiceClass = (AllServices as any)[serviceName];
228
329
 
229
330
  // Create a proxy for each service
@@ -236,7 +337,7 @@ export class NextPlusSDK {
236
337
  return new Proxy(ServiceClass, {
237
338
  get(target, prop) {
238
339
  if (typeof target[prop] === 'function') {
239
- return async function(...args: any[]) {
340
+ return async function (...args: any[]) {
240
341
  // Automatically inject client if not provided
241
342
  if (!args[0]) args[0] = {};
242
343
  // Normalize developer-friendly input into OpenAPI options
@@ -246,11 +347,25 @@ export class NextPlusSDK {
246
347
  const result = await target[prop].apply(target, args);
247
348
 
248
349
  // Return based on returnType setting
249
- if (self.returnType === 'data' && result && typeof result === 'object' && 'data' in result) {
350
+ if (
351
+ self.returnType === 'data' &&
352
+ result &&
353
+ typeof result === 'object' &&
354
+ 'data' in result
355
+ ) {
250
356
  return result.data;
251
357
  }
252
- if (self.returnType === 'data' && result && typeof result === 'object' && 'error' in result) {
253
- const error = new Error(result.error.error.message || result.error.error.code || 'Unknown error') as any;
358
+ if (
359
+ self.returnType === 'data' &&
360
+ result &&
361
+ typeof result === 'object' &&
362
+ 'error' in result
363
+ ) {
364
+ const error = new Error(
365
+ result.error.error.message ||
366
+ result.error.error.code ||
367
+ 'Unknown error',
368
+ ) as any;
254
369
  if (result.error.error.details) {
255
370
  error.details = result.error.error.details;
256
371
  }
@@ -261,46 +376,111 @@ export class NextPlusSDK {
261
376
  };
262
377
  }
263
378
  return target[prop];
264
- }
379
+ },
265
380
  });
266
381
  }
267
382
 
268
- // Manual login method (optional, for explicit control)
383
+ // Manual login method (optional, for explicit control in credentials mode)
269
384
  async manualLogin(): Promise<boolean> {
385
+ if (this.authMode !== 'credentials') {
386
+ throw new Error('manualLogin is only available in credentials mode');
387
+ }
270
388
  return this.login();
271
389
  }
390
+
391
+ // Bind a service to this SDK's client for tree-shakeable imports
392
+ // This eliminates the need to pass client on every call
393
+ service<T extends Record<string, any>>(ServiceClass: T): T {
394
+ const client = this.client;
395
+ const boundService = Object.create(ServiceClass);
396
+
397
+ // Get all property names including static methods
398
+ const properties = Object.getOwnPropertyNames(ServiceClass);
399
+
400
+ properties.forEach((key) => {
401
+ const descriptor = Object.getOwnPropertyDescriptor(ServiceClass, key);
402
+ if (
403
+ descriptor &&
404
+ typeof descriptor.value === 'function' &&
405
+ key !== 'constructor'
406
+ ) {
407
+ // Bind static methods to auto-inject client
408
+ boundService[key] = (options: any = {}) => {
409
+ return descriptor.value.call(ServiceClass, { ...options, client });
410
+ };
411
+ }
412
+ });
413
+
414
+ return boundService;
415
+ }
272
416
  }
273
417
 
274
418
  // Expose accurate static service method types on the SDK instance for IDE/type safety
275
419
  type ServiceStatics = {
276
- [K in keyof typeof AllServiceTypes as K extends `${string}Service` ? K : never]: typeof AllServiceTypes[K];
420
+ [K in keyof typeof AllServiceTypes as K extends `${string}Service`
421
+ ? K
422
+ : never]: (typeof AllServiceTypes)[K];
277
423
  };
278
424
 
279
425
  export interface NextPlusSDK extends ServiceStatics {}
280
426
 
281
- // Legacy function for backward compatibility
427
+ // SDK instance type
282
428
  type SDKInstance = NextPlusSDK & ServiceStatics;
283
429
 
284
- const configure = (options: {
285
- baseURL: string;
286
- email?: string;
287
- username?: string;
288
- password: string;
289
- returnType?: 'raw' | 'data';
290
- }): SDKInstance => {
291
- return new NextPlusSDK(options) as SDKInstance;
430
+ // Import the generated default client to update it
431
+ import { client as generatedClient } from './sdk/client.gen';
432
+
433
+ // Configure function - creates a full SDK instance with service proxies
434
+ // Automatically sets the client as global default for tree-shakeable imports
435
+ const configure = (options: SDKConfig): SDKInstance => {
436
+ const sdk = new NextPlusSDK(options) as SDKInstance;
437
+ // Automatically set as global default
438
+ setDefaultClient(sdk.client);
439
+ // Update the generated SDK's default client so services work without passing client
440
+ Object.assign(generatedClient, sdk.client);
441
+ return sdk;
292
442
  };
293
443
 
294
- // Create SDK instance
295
- const createSDK = (options: {
296
- baseURL: string;
297
- email?: string;
298
- username?: string;
299
- password: string;
300
- returnType?: 'raw' | 'data';
301
- }): SDKInstance => {
302
- return new NextPlusSDK(options) as SDKInstance;
444
+ // Create SDK instance - alias for configure
445
+ const createSDK = (options: SDKConfig): SDKInstance => {
446
+ const sdk = new NextPlusSDK(options) as SDKInstance;
447
+ // Automatically set as global default
448
+ setDefaultClient(sdk.client);
449
+ // Update the generated SDK's default client so services work without passing client
450
+ Object.assign(generatedClient, sdk.client);
451
+ return sdk;
303
452
  };
304
453
 
454
+ // Global default client for tree-shakeable imports
455
+ // Automatically set when configure() or createSDK() is called
456
+ let globalClient: any = null;
457
+
458
+ /**
459
+ * Set a global default client for all service calls
460
+ * Note: This is automatically called by configure() and createSDK()
461
+ * You usually don't need to call this manually
462
+ */
463
+ export function setDefaultClient(client: any): void {
464
+ globalClient = client;
465
+ }
466
+
467
+ /**
468
+ * Get the current global default client
469
+ */
470
+ export function getDefaultClient(): any {
471
+ return globalClient;
472
+ }
473
+
474
+ /**
475
+ * Clear the global default client (useful for logout)
476
+ */
477
+ export function clearDefaultClient(): void {
478
+ globalClient = null;
479
+ }
480
+
305
481
  export type { SDKInstance };
306
482
  export { configure, createSDK };
483
+
484
+ // Re-export SDK types and generated services
485
+ // Services will automatically use global client as fallback via the generated code's default client
486
+ export * from './sdk';