@nexical/sdk-core 0.1.0 → 0.1.1
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/.skills/sdk-core-client-foundation/SKILL.md +78 -0
- package/.skills/sdk-core-client-foundation/examples/usage.ts +62 -0
- package/.skills/sdk-core-client-foundation/templates/auth-strategy.tsf +16 -0
- package/.skills/sdk-core-client-foundation/templates/client-base.tsf +90 -0
- package/.skills/sdk-core-client-foundation/templates/error-handler.tsf +45 -0
- package/.skills/sdk-core-esm-imports/SKILL.md +51 -0
- package/.skills/sdk-core-esm-imports/examples/esm-import.ts +27 -0
- package/.skills/sdk-core-esm-imports/templates/resource.tsf +30 -0
- package/.skills/sdk-core-query-serialization/SKILL.md +36 -0
- package/.skills/sdk-core-query-serialization/examples/serialization-example.ts +44 -0
- package/.skills/sdk-core-query-serialization/templates/resource.tsf +46 -0
- package/.skills/sdk-core-resource-pattern/SKILL.md +64 -0
- package/.skills/sdk-core-resource-pattern/examples/esm-imports.ts +24 -0
- package/.skills/sdk-core-resource-pattern/examples/serialization.ts +41 -0
- package/.skills/sdk-core-resource-pattern/templates/resource-method.tsf +22 -0
- package/.skills/sdk-core-resource-pattern/templates/resource.tsf +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Skill: SDK Core Client Foundation
|
|
2
|
+
|
|
3
|
+
This skill governs the implementation and extension of the core SDK infrastructure within the Nexical Ecosystem. It defines the foundational `ApiClient`, error handling mechanisms, and authentication strategies that power all module-specific SDKs.
|
|
4
|
+
|
|
5
|
+
## 1. Core Principles
|
|
6
|
+
|
|
7
|
+
### Named Export Infrastructure
|
|
8
|
+
|
|
9
|
+
Core SDK infrastructure components (interfaces, classes, errors) MUST use named exports. This ensures explicit importing, better IDE support, and efficient tree-shaking.
|
|
10
|
+
|
|
11
|
+
- **Rule**: Default exports are strictly forbidden in the `sdk-core` package.
|
|
12
|
+
|
|
13
|
+
### Strict Type Safety (No `any`)
|
|
14
|
+
|
|
15
|
+
In accordance with `CODE.md`, the `any` type is strictly forbidden.
|
|
16
|
+
|
|
17
|
+
- **Rule**: Use `unknown` for dynamic request bodies and error data. Use concrete interfaces or properly constrained generics for all other structures.
|
|
18
|
+
|
|
19
|
+
### ESM Compliance
|
|
20
|
+
|
|
21
|
+
- **Rule**: All relative imports MUST include the `.js` extension to satisfy ESM runtime requirements.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. Architectural Patterns
|
|
26
|
+
|
|
27
|
+
### Asynchronous Auth Strategy
|
|
28
|
+
|
|
29
|
+
Authentication logic is decoupled from the `ApiClient` via the `AuthStrategy` interface. This allows for polymorphic authentication (Bearer tokens, API Keys, or Agent-based auth).
|
|
30
|
+
|
|
31
|
+
- **Implementation**: The `ApiClient` calls `authStrategy.getHeaders()` dynamically for every request.
|
|
32
|
+
- **Pattern**: `export interface AuthStrategy { getHeaders(): Promise<Record<string, string>>; }`
|
|
33
|
+
|
|
34
|
+
### Structured Error Handling (`NexicalError`)
|
|
35
|
+
|
|
36
|
+
All API errors MUST be wrapped in the `NexicalError` class. This class is responsible for extracting error messages from various JSON structures (e.g., `error`, `message`) and preserving the HTTP status context.
|
|
37
|
+
|
|
38
|
+
- **Pattern**: `throw new NexicalError(response.status, response.statusText, errorData);`
|
|
39
|
+
|
|
40
|
+
### Generic Request Orchestration
|
|
41
|
+
|
|
42
|
+
All API requests MUST use the generic `request<T>` method provided by the `ApiClient`. This method handles boilerplate such as:
|
|
43
|
+
|
|
44
|
+
- Base URL normalization.
|
|
45
|
+
- Header merging (including dynamic auth headers).
|
|
46
|
+
- JSON stringification for bodies.
|
|
47
|
+
- Environment-aware credential inclusion (defaulting to `include`).
|
|
48
|
+
- **Rule**: Direct use of the global `fetch` is forbidden within the SDK layer.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3. Operational "Gotchas"
|
|
53
|
+
|
|
54
|
+
### 204 No Content Compatibility
|
|
55
|
+
|
|
56
|
+
Requests returning a `204 No Content` status MUST return an empty object (`{}`) cast to the expected generic type `T`. This prevents "Unexpected end of JSON input" errors during parsing.
|
|
57
|
+
|
|
58
|
+
- **Pattern**: `if (response.status === 204) { return {} as T; }`
|
|
59
|
+
|
|
60
|
+
### Base URL Normalization
|
|
61
|
+
|
|
62
|
+
The `ApiClient` constructor MUST normalize the `baseUrl` to remove any trailing slashes. This prevents double-slashes or malformed paths during path concatenation.
|
|
63
|
+
|
|
64
|
+
- **Pattern**: `this.baseUrl = options.baseUrl.replace(/\/$/, '');`
|
|
65
|
+
|
|
66
|
+
### Default Credential Inclusion
|
|
67
|
+
|
|
68
|
+
The client defaults to `include` for credentials to ensure cookies/auth headers are sent by default in browser environments.
|
|
69
|
+
|
|
70
|
+
- **Rule**: The `ApiClient` MUST default to `include` for credentials unless explicitly overridden in `RequestInit`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 4. Centralized SDK Integration
|
|
75
|
+
|
|
76
|
+
The core foundation supports the **Centralized SDK Mandate** defined in `ARCHITECTURE.md`. All module-specific SDKs are aggregated into a global `api` object.
|
|
77
|
+
|
|
78
|
+
- **Requirement**: Use the `ApiClient` as the engine for all module SDK resources.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docker/Dockerfile example-usage.ts
|
|
3
|
+
* @description Example demonstrating the implementation of the SDK Core patterns.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ApiClient } from './client.js';
|
|
7
|
+
import type { AuthStrategy } from './auth.js';
|
|
8
|
+
import { NexicalError } from './errors.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Example of a Bearer Token Auth Strategy.
|
|
12
|
+
*/
|
|
13
|
+
class BearerAuthStrategy implements AuthStrategy {
|
|
14
|
+
constructor(private readonly token: string) {}
|
|
15
|
+
|
|
16
|
+
async getHeaders(): Promise<Record<string, string>> {
|
|
17
|
+
return {
|
|
18
|
+
Authorization: `Bearer ${this.token}`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Example of consuming the ApiClient in a Resource class.
|
|
25
|
+
*/
|
|
26
|
+
class UserResource {
|
|
27
|
+
constructor(private readonly client: ApiClient) {}
|
|
28
|
+
|
|
29
|
+
async getProfile(userId: string): Promise<unknown> {
|
|
30
|
+
try {
|
|
31
|
+
// Pattern: Generic Request Orchestration
|
|
32
|
+
return await this.client.request<unknown>('GET', `/users/${userId}`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof NexicalError) {
|
|
35
|
+
// Pattern: Structured Error Handling
|
|
36
|
+
console.error(`API Error (${error.status}): ${error.message}`);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async deleteUser(userId: string): Promise<void> {
|
|
44
|
+
// Pattern: 204 No Content Compatibility
|
|
45
|
+
// The request method returns {} for 204, satisfying the generic T (here void-ish)
|
|
46
|
+
await this.client.request<void>('DELETE', `/users/${userId}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialization
|
|
51
|
+
const auth = new BearerAuthStrategy('my-secret-token');
|
|
52
|
+
const client = new ApiClient({
|
|
53
|
+
baseUrl: 'https://api.nexical.com/', // Pattern: Base URL Normalization handles the slash
|
|
54
|
+
authStrategy: auth,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const userResource = new UserResource(client);
|
|
58
|
+
|
|
59
|
+
// Demonstrate usage to satisfy linting
|
|
60
|
+
userResource.getProfile('current-user').catch(() => {
|
|
61
|
+
/* Handle or ignore for example purposes */
|
|
62
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docker/Dockerfile auth-strategy.tsf
|
|
3
|
+
* @description Interface definition for AuthStrategy.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pattern: Asynchronous Auth Strategy
|
|
8
|
+
* Allows for polymorphic authentication resolved dynamically.
|
|
9
|
+
*/
|
|
10
|
+
export interface AuthStrategy {
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the headers required for authentication.
|
|
13
|
+
* @returns A promise resolving to a record of header keys and values.
|
|
14
|
+
*/
|
|
15
|
+
getHeaders(): Promise<Record<string, string>>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docker/Dockerfile client-base.tsf
|
|
3
|
+
* @description Fragment for the base ApiClient class.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthStrategy } from './auth.js';
|
|
7
|
+
import { NexicalError } from './errors.js';
|
|
8
|
+
|
|
9
|
+
export interface ApiClientOptions {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
authStrategy: AuthStrategy;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pattern: Named Export Infrastructure
|
|
16
|
+
* The core ApiClient uses named exports and handles request orchestration.
|
|
17
|
+
*/
|
|
18
|
+
export class ApiClient {
|
|
19
|
+
private readonly baseUrl: string;
|
|
20
|
+
private readonly authStrategy: AuthStrategy;
|
|
21
|
+
|
|
22
|
+
constructor(options: ApiClientOptions) {
|
|
23
|
+
/**
|
|
24
|
+
* Pattern: Base URL Normalization
|
|
25
|
+
* Normalizes the baseUrl to remove trailing slashes.
|
|
26
|
+
*/
|
|
27
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
28
|
+
this.authStrategy = options.authStrategy;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pattern: Generic Request Orchestration
|
|
33
|
+
* Orchestrates the API request lifecycle with full type safety.
|
|
34
|
+
*/
|
|
35
|
+
async request<T>(
|
|
36
|
+
method: string,
|
|
37
|
+
path: string,
|
|
38
|
+
body?: unknown,
|
|
39
|
+
options: RequestInit = {}
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
const authHeaders = await this.authStrategy.getHeaders();
|
|
42
|
+
const url = `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
43
|
+
|
|
44
|
+
const config: RequestInit = {
|
|
45
|
+
...options,
|
|
46
|
+
method,
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
...authHeaders,
|
|
50
|
+
...options.headers,
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Pattern: Default Credential Inclusion
|
|
54
|
+
* Default to 'include' for cookies/auth headers in browser environments.
|
|
55
|
+
*/
|
|
56
|
+
credentials: options.credentials || 'include',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (body) {
|
|
60
|
+
config.body = JSON.stringify(body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await fetch(url, config);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Pattern: 204 No Content Compatibility
|
|
67
|
+
* Return an empty object to prevent JSON parsing errors.
|
|
68
|
+
*/
|
|
69
|
+
if (response.status === 204) {
|
|
70
|
+
return {} as T;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
let errorData: unknown;
|
|
75
|
+
try {
|
|
76
|
+
errorData = await response.json();
|
|
77
|
+
} catch {
|
|
78
|
+
errorData = { message: response.statusText };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pattern: Structured Error Handling
|
|
83
|
+
* Wraps API errors in the NexicalError class.
|
|
84
|
+
*/
|
|
85
|
+
throw new NexicalError(response.status, response.statusText, errorData);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return response.json() as Promise<T>;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docker/Dockerfile error-handler.tsf
|
|
3
|
+
* @description The NexicalError implementation pattern.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pattern: Structured Error Handling (NexicalError)
|
|
8
|
+
* Standardized error wrapper that extracts messages from various JSON structures.
|
|
9
|
+
*/
|
|
10
|
+
export class NexicalError extends Error {
|
|
11
|
+
public readonly status: number;
|
|
12
|
+
public readonly statusText: string;
|
|
13
|
+
public readonly data: unknown;
|
|
14
|
+
|
|
15
|
+
constructor(status: number, statusText: string, data: unknown) {
|
|
16
|
+
const message = NexicalError.extractMessage(data) || statusText || 'API Error';
|
|
17
|
+
super(message);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.statusText = statusText;
|
|
20
|
+
this.data = data;
|
|
21
|
+
this.name = 'NexicalError';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract an error message from a dynamic JSON response body.
|
|
26
|
+
* @param data - The error payload (using unknown to avoid 'any').
|
|
27
|
+
* @returns A string error message if found, otherwise undefined.
|
|
28
|
+
*/
|
|
29
|
+
private static extractMessage(data: unknown): string | undefined {
|
|
30
|
+
if (typeof data === 'string') {
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (data && typeof data === 'object') {
|
|
35
|
+
const payload = data as Record<string, unknown>;
|
|
36
|
+
return (
|
|
37
|
+
(payload.error as string) ||
|
|
38
|
+
(payload.message as string) ||
|
|
39
|
+
(payload.msg as string)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Skill: SDK ESM Imports & Resource Standards
|
|
2
|
+
|
|
3
|
+
This skill enforces strict ESM compatibility and architectural consistency within the `sdk-core` package.
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
The `sdk-core` package provides the foundational primitives for the Nexical SDK. It must strictly adhere to ESM runtime requirements (specifically explicit file extensions) and follow the Abstract Resource pattern for consistent request orchestration.
|
|
8
|
+
|
|
9
|
+
## Standards
|
|
10
|
+
|
|
11
|
+
### 1. Strict ESM Relative Imports
|
|
12
|
+
|
|
13
|
+
All relative imports MUST include the explicit `.js` extension. This is required for Node.js ESM resolution and compatibility across the ecosystem.
|
|
14
|
+
|
|
15
|
+
- **Rule**: Relative imports MUST end in `.js`.
|
|
16
|
+
- **Example**: `import { ApiClient } from './client.js';`
|
|
17
|
+
|
|
18
|
+
### 2. Abstract SDK Resource Pattern
|
|
19
|
+
|
|
20
|
+
SDK functional areas are organized into 'Resource' classes that extend `BaseResource`.
|
|
21
|
+
|
|
22
|
+
- **Rule**: Every SDK resource MUST extend `BaseResource`.
|
|
23
|
+
- **Rule**: Resources MUST receive an `ApiClient` via constructor injection.
|
|
24
|
+
- **Example**:
|
|
25
|
+
```typescript
|
|
26
|
+
export class UserResource extends BaseResource {
|
|
27
|
+
constructor(client: ApiClient) {
|
|
28
|
+
super(client);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Nested Query Parameter Serialization
|
|
34
|
+
|
|
35
|
+
Filtering nested data structures requires a specific flattening logic for backend compatibility.
|
|
36
|
+
|
|
37
|
+
- **Rule**: Nested filter objects MUST be processed recursively using the double-underscore (`__`) delimiter for key concatenation.
|
|
38
|
+
- **Implementation**: Handled by the `buildQuery` utility in `sdk-core`.
|
|
39
|
+
- **Example**: `{ filter: { user: { name: 'test' } } }` -> `?filter__user__name=test`
|
|
40
|
+
|
|
41
|
+
### 4. Zero-Tolerance for 'any'
|
|
42
|
+
|
|
43
|
+
Preserve type safety by avoiding the `any` type at all costs.
|
|
44
|
+
|
|
45
|
+
- **Rule**: The `any` type is strictly forbidden.
|
|
46
|
+
- **Rule**: Use `unknown` or `Record<string, unknown>` for unpredictable payloads.
|
|
47
|
+
|
|
48
|
+
## Resources
|
|
49
|
+
|
|
50
|
+
- **Templates**: `templates/resource.tsf`
|
|
51
|
+
- **Examples**: `examples/esm-import.ts`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packages/sdk-core/.skills/sdk-core-esm-imports/SKILL.md sdk-core-esm-imports
|
|
3
|
+
* @description Demonstrates correct ESM relative import usage with .js extensions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// CORRECT: ESM requires the explicit .js extension for relative imports.
|
|
7
|
+
import { ApiClient } from './client.js';
|
|
8
|
+
import { BaseResource } from './resource.js';
|
|
9
|
+
import { buildQuery } from '../utils/query.js';
|
|
10
|
+
|
|
11
|
+
// INCORRECT (Common AI Hallucination): Omitting the extension.
|
|
12
|
+
// import { ApiClient } from './client';
|
|
13
|
+
|
|
14
|
+
export class ExampleResource extends BaseResource {
|
|
15
|
+
constructor(client: ApiClient) {
|
|
16
|
+
super(client);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async list(filters: Record<string, unknown>): Promise<unknown[]> {
|
|
20
|
+
// Usage of buildQuery to satisfy lint rules and demonstrate pattern
|
|
21
|
+
const query = buildQuery({ filter: filters });
|
|
22
|
+
return this.client.request<unknown[]>({
|
|
23
|
+
method: 'GET',
|
|
24
|
+
url: `/example?${query}`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packages/sdk-core/.skills/sdk-core-esm-imports/SKILL.md sdk-core-esm-imports
|
|
3
|
+
* @packages/generator/src/utils/fragment-tag.ts New SDK Resource Template
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseResource } from './resource.js';
|
|
7
|
+
import type { ApiClient } from './client.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* [RESOURCE_NAME] resource implementation.
|
|
11
|
+
* All SDK resource areas MUST extend BaseResource to inherit request orchestration logic.
|
|
12
|
+
*/
|
|
13
|
+
export class [RESOURCE_NAME] extends BaseResource {
|
|
14
|
+
/**
|
|
15
|
+
* @param client - The API client injected via constructor.
|
|
16
|
+
*/
|
|
17
|
+
constructor(client: ApiClient) {
|
|
18
|
+
super(client);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Example method demonstrating the standard request flow.
|
|
23
|
+
*/
|
|
24
|
+
public async get[ENTITY](id: string): Promise<[ENTITY]Type> {
|
|
25
|
+
return this.client.request<[ENTITY]Type>({
|
|
26
|
+
method: 'GET',
|
|
27
|
+
url: `/[PATH]/${id}`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Skill: SDK Core Query Serialization
|
|
2
|
+
|
|
3
|
+
Expert guide for implementing type-safe SDK resources with nested query parameter serialization. This skill ensures that all SDK modules follow the "Shell-Registry" compatible communication pattern.
|
|
4
|
+
|
|
5
|
+
## Core Patterns
|
|
6
|
+
|
|
7
|
+
### 1. Abstract Resource Pattern
|
|
8
|
+
|
|
9
|
+
All SDK functional areas MUST be organized into 'Resource' classes that extend the `BaseResource` abstract class. This ensures consistent request orchestration and centralized logic for query building.
|
|
10
|
+
|
|
11
|
+
- **Rule**: Every resource MUST receive an `ApiClient` via constructor injection.
|
|
12
|
+
- **Rule**: Use named exports for all resource classes.
|
|
13
|
+
|
|
14
|
+
### 2. Nested Query Parameter Serialization
|
|
15
|
+
|
|
16
|
+
The SDK utilizes a recursive serialization utility to flatten complex filter objects into URL search parameters. This maintains compatibility with the backend's `parseQuery` utility.
|
|
17
|
+
|
|
18
|
+
- **Delimiter**: Use a double-underscore (`__`) to represent nesting (e.g., `filter: { user: { name: 'Alice' } }` -> `?filter__user__name=Alice`).
|
|
19
|
+
- **Rule**: All `list` or `find` methods in a resource MUST use this serialization logic before making the request.
|
|
20
|
+
|
|
21
|
+
### 3. NexicalError Strategy
|
|
22
|
+
|
|
23
|
+
API errors must be captured and transformed into `NexicalError` instances to preserve HTTP status context and provide standardized error messages.
|
|
24
|
+
|
|
25
|
+
- **Rule**: The SDK must handle both `error` (translation key) and `message` (human-readable) fields from the API response.
|
|
26
|
+
|
|
27
|
+
## Implementation Standards
|
|
28
|
+
|
|
29
|
+
- **Zero Any**: Use `unknown` for generic payloads and `Record<string, unknown>` for structured but dynamic filter objects.
|
|
30
|
+
- **ESM Relative Imports**: All internal imports MUST include the `.js` extension.
|
|
31
|
+
- **Named Exports**: Default exports are strictly forbidden in core SDK packages.
|
|
32
|
+
|
|
33
|
+
## Troubleshooting
|
|
34
|
+
|
|
35
|
+
- **Serialization Issues**: Verify that the `__` delimiter is correctly applied to nested keys.
|
|
36
|
+
- **204 Responses**: Ensure that HTTP 204 (No Content) responses return a valid `ServiceResponse` structure (e.g., `{ success: true, data: {} as T }`) to satisfy type contracts.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Nested Query Parameter Serialization
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how a complex filter object is flattened
|
|
5
|
+
* into URL search parameters using the double-underscore (__) delimiter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recursive utility to build query parameters from nested objects.
|
|
10
|
+
*/
|
|
11
|
+
function buildQueryParams(
|
|
12
|
+
params: URLSearchParams,
|
|
13
|
+
obj: Record<string, unknown>,
|
|
14
|
+
prefix: string = '',
|
|
15
|
+
): void {
|
|
16
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
17
|
+
const fullKey = prefix ? `${prefix}__${key}` : key;
|
|
18
|
+
|
|
19
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
20
|
+
buildQueryParams(params, value as Record<string, unknown>, fullKey);
|
|
21
|
+
} else {
|
|
22
|
+
params.append(fullKey, String(value));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Example Usage:
|
|
28
|
+
const filters = {
|
|
29
|
+
status: 'active',
|
|
30
|
+
user: {
|
|
31
|
+
role: 'admin',
|
|
32
|
+
profile: {
|
|
33
|
+
isVerified: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
tags: ['featured', 'new'],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
buildQueryParams(params, filters);
|
|
41
|
+
|
|
42
|
+
console.log(params.toString());
|
|
43
|
+
// Output:
|
|
44
|
+
// status=active&user__role=admin&user__profile__isVerified=true&tags=featured%2Cnew
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @core/.skills/sdk-core-query-serialization/SKILL.md {import('./resource.js').BaseResource}
|
|
3
|
+
*
|
|
4
|
+
* Fragment Contract:
|
|
5
|
+
* 1. JSDoc header MUST reference the SDK-Core-Query-Serialization skill.
|
|
6
|
+
* 2. Exports the `fragment` tagged template literal.
|
|
7
|
+
* 3. Uses placeholder variables: `${name}`, `${entity}`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fragment } from '@/lib/generator/fragment.js';
|
|
11
|
+
|
|
12
|
+
export default fragment`
|
|
13
|
+
import { BaseResource } from '../resource.js';
|
|
14
|
+
import type { ServiceResponse } from '@nexical/sdk-core';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SDK Resource for the ${name} entity.
|
|
18
|
+
*/
|
|
19
|
+
export class ${name}Resource extends BaseResource {
|
|
20
|
+
/**
|
|
21
|
+
* List ${entity} records with optional filtering and pagination.
|
|
22
|
+
* @param filters - Nested filter object using the standard __ delimiter.
|
|
23
|
+
* @returns A promise resolving to a ServiceResponse containing the results.
|
|
24
|
+
*/
|
|
25
|
+
public async list(filters?: Record<string, unknown>): Promise<ServiceResponse<unknown[]>> {
|
|
26
|
+
const params = this.buildQueryParams(filters);
|
|
27
|
+
return this.client.request<ServiceResponse<unknown[]>>({
|
|
28
|
+
method: 'GET',
|
|
29
|
+
path: '/api/${entity}/list',
|
|
30
|
+
params,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Retrieve a single ${entity} by ID.
|
|
36
|
+
* @param id - The unique identifier of the record.
|
|
37
|
+
* @returns A promise resolving to a ServiceResponse containing the entity.
|
|
38
|
+
*/
|
|
39
|
+
public async get(id: string): Promise<ServiceResponse<unknown>> {
|
|
40
|
+
return this.client.request<ServiceResponse<unknown>>({
|
|
41
|
+
method: 'GET',
|
|
42
|
+
path: `/api/\${entity}/get/\${id}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Abstract Resource Pattern (sdk-core)
|
|
2
|
+
|
|
3
|
+
This skill governs the implementation of SDK functional areas (Resources) within the `sdk-core` package. It ensures that all data access logic is organized into standardized, type-safe classes that utilize centralized request orchestration and query serialization.
|
|
4
|
+
|
|
5
|
+
## Core Mandates
|
|
6
|
+
|
|
7
|
+
1. **Abstract Resource Pattern**: All resource classes MUST extend `BaseResource` and receive an `ApiClient` instance via constructor injection.
|
|
8
|
+
2. **Generic Request Orchestration**: Use the protected `_request<T>` method to wrap the client's request logic. This provides a type-safe interface and ensures consistent error handling.
|
|
9
|
+
3. **Nested Query Parameter Serialization**: Query parameter serialization MUST use the double-underscore (`__`) delimiter for nested object keys (e.g., `filter__user__name=Alice`) to ensure compatibility with the backend's `parseQuery` utility.
|
|
10
|
+
4. **ESM Module Imports**: All relative imports MUST include the `.js` extension (e.g., `import { BaseResource } from './resource.js';`).
|
|
11
|
+
5. **Zero-Tolerance for 'any'**: The use of `any` is strictly forbidden. Use specific interfaces or `unknown` for dynamic data like request bodies or filter values.
|
|
12
|
+
6. **Infrastructure Named Exports**: Default exports are forbidden. All components MUST be exported as named members to ensure explicit importing and better tree-shaking.
|
|
13
|
+
7. **Auth Strategy Integration**: Authentication headers MUST be resolved dynamically via the injected `AuthStrategy` in the `ApiClient`.
|
|
14
|
+
8. **204 No Content Compatibility**: Handlers MUST explicitly check for 204 status codes and return an empty object (`{}`) cast to the expected type `T`.
|
|
15
|
+
9. **NexicalError Handling**: All API errors MUST be wrapped in the `NexicalError` class to preserve HTTP status context.
|
|
16
|
+
10. **Base URL Normalization**: Base URLs MUST be normalized in the constructor to remove trailing slashes to prevent path concatenation errors.
|
|
17
|
+
11. **Default Credential Inclusion**: The `ApiClient` MUST default to 'include' for credentials unless explicitly overridden to ensure cookies/auth headers are sent in browser environments.
|
|
18
|
+
|
|
19
|
+
## Implementation Workflow
|
|
20
|
+
|
|
21
|
+
### 1. Define the Resource Class
|
|
22
|
+
|
|
23
|
+
Create a new file in `src/resources/` (or similar) that extends `BaseResource`.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { BaseResource } from '../resource.js';
|
|
27
|
+
import type { ApiClient } from '../client.js';
|
|
28
|
+
|
|
29
|
+
export class MyResource extends BaseResource {
|
|
30
|
+
constructor(client: ApiClient) {
|
|
31
|
+
super(client);
|
|
32
|
+
// BaseResource constructor handles Base URL Normalization
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Implement Operation Methods
|
|
38
|
+
|
|
39
|
+
Use the `_request<T>` wrapper for all API calls.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export class MyResource extends BaseResource {
|
|
43
|
+
async get(id: string): Promise<MyDataType> {
|
|
44
|
+
return this._request<MyDataType>('GET', `/my-resource/${id}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Handle Queries and Filters
|
|
50
|
+
|
|
51
|
+
Use the `buildQuery` method (provided by `BaseResource`) which implements the double-underscore serialization logic.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
async list(filters: Record<string, unknown> = {}): Promise<MyDataType[]> {
|
|
55
|
+
const query = this.buildQuery(filters);
|
|
56
|
+
return this._request<MyDataType[]>('GET', `/my-resource${query}`);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Reference Patterns
|
|
61
|
+
|
|
62
|
+
- **Serialization**: Refer to `examples/serialization.ts` for nested object flattening logic.
|
|
63
|
+
- **ESM Imports**: Refer to `examples/esm-imports.ts` for correct import syntax.
|
|
64
|
+
- **Resource Structure**: Refer to `templates/resource.tsf` for the standard class boilerplate.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: ESM Module Imports (.js extension)
|
|
3
|
+
*
|
|
4
|
+
* Requirement: All relative imports MUST include the '.js' extension.
|
|
5
|
+
* Reason: ESM runtime requirements and Bun/Node.js compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// CORRECT: Including .js extension even for .ts source files
|
|
9
|
+
import { ApiClient as _ApiClient } from './client.js';
|
|
10
|
+
import { BaseResource } from './resource.js';
|
|
11
|
+
import { NexicalError as _NexicalError } from '../errors/index.js';
|
|
12
|
+
|
|
13
|
+
// INCORRECT: Missing extension (will fail in ESM)
|
|
14
|
+
// import { ApiClient } from './client';
|
|
15
|
+
|
|
16
|
+
// INCORRECT: Using .ts extension (not supported by ESM loaders)
|
|
17
|
+
// import { ApiClient } from './client.ts';
|
|
18
|
+
|
|
19
|
+
export class UserResource extends BaseResource {
|
|
20
|
+
// Example usage of BaseResource to avoid empty class if needed
|
|
21
|
+
async getProfile() {
|
|
22
|
+
return this._request('GET', '/profile');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Nested Query Parameter Serialization
|
|
3
|
+
*
|
|
4
|
+
* Requirement: Flatten nested objects using double-underscore (__) delimiter.
|
|
5
|
+
* Compatibility: backend parseQuery utility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const filters = {
|
|
9
|
+
status: 'active',
|
|
10
|
+
user: {
|
|
11
|
+
name: 'Alice',
|
|
12
|
+
role: {
|
|
13
|
+
type: 'admin',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Resulting query string should look like:
|
|
19
|
+
// ?status=active&user__name=Alice&user__role__type=admin
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Simplified logic representation of BaseResource.buildQuery
|
|
23
|
+
*/
|
|
24
|
+
function buildQuery(filters: Record<string, unknown>, prefix = ''): string {
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
|
|
27
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
28
|
+
const fullKey = prefix ? `${prefix}__${key}` : key;
|
|
29
|
+
|
|
30
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
31
|
+
// Recursively flatten nested objects
|
|
32
|
+
parts.push(buildQuery(value as Record<string, unknown>, fullKey));
|
|
33
|
+
} else {
|
|
34
|
+
parts.push(`${fullKey}=${encodeURIComponent(String(value))}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return parts.join('&');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.info('?' + buildQuery(filters));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fragment ResourceMethodTemplate
|
|
3
|
+
* @description Standard implementation for a resource operation using _request and buildQuery.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* PHANTOM_DECLARATIONS_START */
|
|
7
|
+
import { BaseResource } from '../resource.js';
|
|
8
|
+
class MyResource extends BaseResource {
|
|
9
|
+
_request<T>(m: string, p: string, b?: unknown): Promise<T> { return {} as any; }
|
|
10
|
+
}
|
|
11
|
+
interface MyDataType { id: string; }
|
|
12
|
+
/* PHANTOM_DECLARATIONS_END */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ${Summary}
|
|
16
|
+
* @param filters - Optional filters for the request.
|
|
17
|
+
* @returns A promise resolving to the data.
|
|
18
|
+
*/
|
|
19
|
+
async ${methodName}(filters: Record<string, unknown> = {}): Promise<${ReturnType}> {
|
|
20
|
+
const query = this.buildQuery(filters);
|
|
21
|
+
return this._request<${ReturnType}>('${verb}', `${path}${query}`);
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fragment ResourceTemplate
|
|
3
|
+
* @description Standard boilerplate for an SDK Resource class extending BaseResource.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseResource } from '../resource.js';
|
|
7
|
+
import type { ApiClient } from '../client.js';
|
|
8
|
+
|
|
9
|
+
/* PHANTOM_DECLARATIONS_START */
|
|
10
|
+
interface MyDataType { id: string; name: string; }
|
|
11
|
+
/* PHANTOM_DECLARATIONS_END */
|
|
12
|
+
|
|
13
|
+
export class ${ResourceName}Resource extends BaseResource {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new instance of ${ResourceName}Resource.
|
|
16
|
+
* @param client - The ApiClient instance for request orchestration.
|
|
17
|
+
*/
|
|
18
|
+
constructor(client: ApiClient) {
|
|
19
|
+
super(client);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Implementation methods follow...
|
|
23
|
+
}
|