sqlcrud-client 1.0.0
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 +159 -0
- package/package.json +43 -0
- package/src/client.js +223 -0
- package/src/errors.js +77 -0
- package/src/index.js +23 -0
- package/src/utils.js +119 -0
- package/types.d.ts +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# sqlcrud-client
|
|
2
|
+
|
|
3
|
+
HTTP client library for the [sqlcrud](https://github.com/bnielsen1965/sqlcrud) SQLite REST API. Works in browsers and Node.js (native `fetch`, zero runtime dependencies).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install sqlcrud-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { SqlCrudClient } from 'sqlcrud-client';
|
|
15
|
+
|
|
16
|
+
const client = new SqlCrudClient({
|
|
17
|
+
baseUrl: 'http://localhost:3123',
|
|
18
|
+
username: 'admin',
|
|
19
|
+
password: 'password',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// List tables
|
|
23
|
+
const tables = await client.listTables();
|
|
24
|
+
|
|
25
|
+
// Create a schema
|
|
26
|
+
await client.setSchema('users', {
|
|
27
|
+
name: { type: 'string', length: 100, primary: true },
|
|
28
|
+
email: { type: 'string', unique: true },
|
|
29
|
+
active: { type: 'boolean' },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Create a record (booleans coerced to 1/0 automatically)
|
|
33
|
+
const user = await client.createRecord('users', {
|
|
34
|
+
name: 'Alice',
|
|
35
|
+
email: 'alice@example.com',
|
|
36
|
+
active: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Query records
|
|
40
|
+
const activeUsers = await client.queryRecords('users', { active: 1 });
|
|
41
|
+
|
|
42
|
+
// Update a record
|
|
43
|
+
const result = await client.updateRecord('users', { name: 'Alice' }, { email: 'new@example.com' });
|
|
44
|
+
console.log(result.before, result.after);
|
|
45
|
+
|
|
46
|
+
// Delete records
|
|
47
|
+
await client.deleteRecord('users', { name: 'Alice' });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Constructor
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
new SqlCrudClient(config)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Option | Type | Required | Description |
|
|
57
|
+
|--------|------|----------|-------------|
|
|
58
|
+
| `baseUrl` | `string` | Yes | sqlcrud server URL (e.g. `'http://localhost:3123'`) |
|
|
59
|
+
| `username` | `string` | No | Username for Basic Auth |
|
|
60
|
+
| `password` | `string` | No | Password for Basic Auth |
|
|
61
|
+
| `headers` | `object` | No | Additional headers sent with every request |
|
|
62
|
+
|
|
63
|
+
## API Methods
|
|
64
|
+
|
|
65
|
+
### Schema Management
|
|
66
|
+
|
|
67
|
+
| Method | HTTP | Description |
|
|
68
|
+
|--------|------|-------------|
|
|
69
|
+
| `listTables()` | `GET /api/tables` | Returns `string[]` of table names |
|
|
70
|
+
| `listModels()` | `GET /api/models` | Returns model info array |
|
|
71
|
+
| `getSchema(model)` | `GET /api/schema/:model` | Returns schema definition object |
|
|
72
|
+
| `setSchema(model, schema)` | `POST /api/schema/:model` | Create or update a schema |
|
|
73
|
+
| `deleteSchema(model)` | `DELETE /api/schema/:model` | Delete schema and drop table |
|
|
74
|
+
|
|
75
|
+
### Record Operations
|
|
76
|
+
|
|
77
|
+
| Method | HTTP | Description |
|
|
78
|
+
|--------|------|-------------|
|
|
79
|
+
| `queryRecords(model, params?)` | `GET /api/record/:model` | Query records by field criteria (returns `[]` if no match) |
|
|
80
|
+
| `createRecord(model, data)` | `POST /api/record/:model` | Create a new record |
|
|
81
|
+
| `updateRecord(model, criteria, data)` | `PUT /api/record/:model` | Update a record (criteria must match exactly one row) |
|
|
82
|
+
| `deleteRecord(model, criteria)` | `DELETE /api/record/:model` | Delete records matching criteria |
|
|
83
|
+
|
|
84
|
+
## Error Handling
|
|
85
|
+
|
|
86
|
+
All errors extend `SqlCrudError` for easy `instanceof` checking:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
import { SqlCrudError, SqlCrudAuthError, SqlCrudNotFoundError } from 'sqlcrud-client';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await client.getSchema('nonexistent');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof SqlCrudAuthError) {
|
|
95
|
+
console.error('Authentication failed');
|
|
96
|
+
} else if (err instanceof SqlCrudNotFoundError) {
|
|
97
|
+
console.error('Model not found');
|
|
98
|
+
} else if (err instanceof SqlCrudError) {
|
|
99
|
+
console.error('Error:', err.message, 'Status:', err.statusCode);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Error Class | When |
|
|
105
|
+
|-------------|------|
|
|
106
|
+
| `SqlCrudError` | Base class for all library errors |
|
|
107
|
+
| `SqlCrudAuthError` | 401 Unauthorized |
|
|
108
|
+
| `SqlCrudNotFoundError` | 404 Not Found |
|
|
109
|
+
| `SqlCrudValidationError` | Client-side validation failure |
|
|
110
|
+
| `SqlCrudServerError` | 5xx server error |
|
|
111
|
+
|
|
112
|
+
All errors expose `statusCode` (number) and `endpoint` (string).
|
|
113
|
+
|
|
114
|
+
## Utility Functions
|
|
115
|
+
|
|
116
|
+
Import standalone (tree-shakeable):
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { validateSchema, coerceBooleans, FIELD_TYPES } from 'sqlcrud-client/utils';
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Validation
|
|
123
|
+
|
|
124
|
+
- `validateIdentifier(name, label?)` — Validates model/field names against `^[a-zA-Z_][a-zA-Z0-9_]*$`
|
|
125
|
+
- `validateFieldName(name)` — Validates field names (identifier + reserved name check)
|
|
126
|
+
- `validateSchema(schema)` — Validates a complete schema definition object
|
|
127
|
+
|
|
128
|
+
### Constants
|
|
129
|
+
|
|
130
|
+
- `FIELD_TYPES` — Valid schema field types: `'string'`, `'integer'`, `'float'`, `'boolean'`, `'json'`, `'datetime'`, `'time'`
|
|
131
|
+
- `RESERVED_FIELD_NAMES` — Reserved field names: `'model'`, `'schema'`
|
|
132
|
+
- `IDENTIFIER_PATTERN` — Regex: `/^[a-zA-Z_][a-zA-Z0-9_]*$/`
|
|
133
|
+
|
|
134
|
+
### Helpers
|
|
135
|
+
|
|
136
|
+
- `coerceBooleans(record)` — Converts `true`/`false` to `1`/`0` in a record (SQLite compatibility; applied automatically by `createRecord` and `updateRecord`)
|
|
137
|
+
- `buildQueryString(params)` — Builds a URL query string from an object
|
|
138
|
+
|
|
139
|
+
## Features
|
|
140
|
+
|
|
141
|
+
- **Boolean coercion** — `createRecord` and `updateRecord` automatically convert JavaScript booleans to `1`/`0` for SQLite compatibility. On read, the server coerces `0`/`1` back to `true`/`false`, so you always work with native booleans.
|
|
142
|
+
- **JSON field support** — The sqlcrud server automatically serializes objects and arrays to JSON strings on write and deserializes them on read. Send native JavaScript objects and arrays directly — no manual `JSON.stringify`/`JSON.parse` needed.
|
|
143
|
+
- **Composite primary keys** — Schemas may define multiple fields with `primary: true`; the server creates composite primary key constraints automatically.
|
|
144
|
+
- **Empty query results** — `queryRecords` returns an empty array (`[]`) when no records match, rather than throwing a `SqlCrudNotFoundError`.
|
|
145
|
+
- **Query parameter building** — Record query methods build and encode URL query parameters automatically.
|
|
146
|
+
- **Basic Auth** — Credentials from the constructor are sent as `Authorization: Basic` on every request.
|
|
147
|
+
- **No dependencies** — Uses native `fetch` (Node.js 18+, all modern browsers).
|
|
148
|
+
|
|
149
|
+
## TypeScript
|
|
150
|
+
|
|
151
|
+
Type definitions are included. Import normally — your TypeScript compiler will pick up `types.d.ts` automatically via the `exports` field in `package.json`.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { SqlCrudClient, SchemaDefinition, SqlCrudError } from 'sqlcrud-client';
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Requirements
|
|
158
|
+
|
|
159
|
+
- Node.js >= 18.0.0 or any modern browser with `fetch` support
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sqlcrud-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HTTP client library for sqlcrud SQLite REST API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./types.d.ts",
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./errors": {
|
|
14
|
+
"types": "./types.d.ts",
|
|
15
|
+
"import": "./src/errors.js",
|
|
16
|
+
"default": "./src/errors.js"
|
|
17
|
+
},
|
|
18
|
+
"./utils": {
|
|
19
|
+
"types": "./types.d.ts",
|
|
20
|
+
"import": "./src/utils.js",
|
|
21
|
+
"default": "./src/utils.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": ["src/", "types.d.ts", "package.json", "README.md"],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"keywords": ["sqlcrud", "sqlite", "rest", "client", "crud", "node"],
|
|
33
|
+
"author": "Bryan Nielsen (@bnielsen1965)",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/bnielsen1965/sqlcrud-client.git"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"vitest": "^2.1.0"
|
|
41
|
+
},
|
|
42
|
+
"sideEffects": false
|
|
43
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { coerceBooleans, buildQueryString } from './utils.js';
|
|
2
|
+
import {
|
|
3
|
+
SqlCrudError,
|
|
4
|
+
SqlCrudAuthError,
|
|
5
|
+
SqlCrudNotFoundError,
|
|
6
|
+
SqlCrudServerError,
|
|
7
|
+
} from './errors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* HTTP client for the sqlcrud REST API.
|
|
11
|
+
* Works in browsers and Node.js (native fetch).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { SqlCrudClient } from 'sqlcrud-client';
|
|
15
|
+
*
|
|
16
|
+
* const client = new SqlCrudClient({
|
|
17
|
+
* baseUrl: 'http://localhost:3123',
|
|
18
|
+
* username: 'admin',
|
|
19
|
+
* password: 'password',
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* await client.setSchema('users', { name: { type: 'string', primary: true } });
|
|
23
|
+
* await client.createRecord('users', { name: 'Alice', active: true });
|
|
24
|
+
*/
|
|
25
|
+
export class SqlCrudClient {
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} config - Client configuration.
|
|
28
|
+
* @param {string} config.baseUrl - The base URL of the sqlcrud server (e.g. 'http://localhost:3123').
|
|
29
|
+
* @param {string} [config.username] - Username for Basic Auth.
|
|
30
|
+
* @param {string} [config.password] - Password for Basic Auth.
|
|
31
|
+
* @param {object} [config.headers] - Additional headers to send with every request.
|
|
32
|
+
*/
|
|
33
|
+
constructor(config) {
|
|
34
|
+
if (!config?.baseUrl) {
|
|
35
|
+
throw new TypeError('SqlCrudClient requires a baseUrl');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @readonly */
|
|
39
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
40
|
+
|
|
41
|
+
/** @readonly */
|
|
42
|
+
this.authHeader =
|
|
43
|
+
config.username && config.password
|
|
44
|
+
? `Basic ${btoa(`${config.username}:${config.password}`)}`
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
/** @readonly */
|
|
48
|
+
this.extraHeaders = config.headers ?? {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────
|
|
52
|
+
// Schema Management
|
|
53
|
+
// ─────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* List all table names in the database.
|
|
57
|
+
* @returns {Promise<string[]>} Array of table names.
|
|
58
|
+
*/
|
|
59
|
+
async listTables() {
|
|
60
|
+
return this._request('GET', '/api/tables');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List all registered models with their schemas.
|
|
65
|
+
* @returns {Promise<Array<{model: string, schema: string}>>} Array of model info objects.
|
|
66
|
+
*/
|
|
67
|
+
async listModels() {
|
|
68
|
+
return this._request('GET', '/api/models');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the schema definition for a model.
|
|
73
|
+
* @param {string} model - The model name.
|
|
74
|
+
* @returns {Promise<object>} The schema definition object.
|
|
75
|
+
* @throws {SqlCrudNotFoundError} If the model does not exist.
|
|
76
|
+
*/
|
|
77
|
+
async getSchema(model) {
|
|
78
|
+
return this._request('GET', `/api/schema/${model}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create or update a schema. Creates the underlying SQLite table if new.
|
|
83
|
+
* @param {string} model - The model name.
|
|
84
|
+
* @param {object} schema - The schema definition object.
|
|
85
|
+
*/
|
|
86
|
+
async setSchema(model, schema) {
|
|
87
|
+
await this._request('POST', `/api/schema/${model}`, { body: schema });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete a schema and drop its table.
|
|
92
|
+
* @param {string} model - The model name.
|
|
93
|
+
*/
|
|
94
|
+
async deleteSchema(model) {
|
|
95
|
+
await this._request('DELETE', `/api/schema/${model}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────
|
|
99
|
+
// Record Operations
|
|
100
|
+
// ─────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Query records matching field criteria (AND-combined).
|
|
104
|
+
* @param {string} model - The model name.
|
|
105
|
+
* @param {object} [params] - Field name → value pairs for WHERE clause.
|
|
106
|
+
* @returns {Promise<object[]>} Array of matching record objects.
|
|
107
|
+
*/
|
|
108
|
+
async queryRecords(model, params) {
|
|
109
|
+
const qs = buildQueryString(params ?? {});
|
|
110
|
+
return this._request('GET', `/api/record/${model}${qs}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a new record. Boolean values are coerced to 0/1 automatically.
|
|
115
|
+
* @param {string} model - The model name.
|
|
116
|
+
* @param {object} data - The record data.
|
|
117
|
+
* @returns {Promise<object>} The created record as persisted (includes rowid).
|
|
118
|
+
*/
|
|
119
|
+
async createRecord(model, data) {
|
|
120
|
+
return this._request('POST', `/api/record/${model}`, {
|
|
121
|
+
body: coerceBooleans(data),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Update a single record. Criteria must match exactly one row.
|
|
127
|
+
* @param {string} model - The model name.
|
|
128
|
+
* @param {object} criteria - Field name → value pairs to identify the target record.
|
|
129
|
+
* @param {object} data - The updated field values.
|
|
130
|
+
* @returns {Promise<{before: object, after: object}>} The record before and after the update.
|
|
131
|
+
*/
|
|
132
|
+
async updateRecord(model, criteria, data) {
|
|
133
|
+
const qs = buildQueryString(criteria);
|
|
134
|
+
return this._request('PUT', `/api/record/${model}${qs}`, {
|
|
135
|
+
body: coerceBooleans(data),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Delete records matching field criteria (deletes all matches).
|
|
141
|
+
* @param {string} model - The model name.
|
|
142
|
+
* @param {object} criteria - Field name → value pairs for WHERE clause.
|
|
143
|
+
*/
|
|
144
|
+
async deleteRecord(model, criteria) {
|
|
145
|
+
const qs = buildQueryString(criteria);
|
|
146
|
+
await this._request('DELETE', `/api/record/${model}${qs}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────
|
|
150
|
+
// Internal Helpers
|
|
151
|
+
// ─────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build request headers for a given method and body presence.
|
|
155
|
+
* @param {string} method - HTTP method.
|
|
156
|
+
* @param {boolean} hasBody - Whether the request has a body.
|
|
157
|
+
* @returns {object} Headers object.
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
_buildHeaders(method, hasBody) {
|
|
161
|
+
const headers = {
|
|
162
|
+
Accept: 'application/json',
|
|
163
|
+
...this.extraHeaders,
|
|
164
|
+
};
|
|
165
|
+
if (this.authHeader) {
|
|
166
|
+
headers['Authorization'] = this.authHeader;
|
|
167
|
+
}
|
|
168
|
+
if (hasBody) {
|
|
169
|
+
headers['Content-Type'] = 'application/json';
|
|
170
|
+
}
|
|
171
|
+
return headers;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Execute an HTTP request and handle responses/errors.
|
|
176
|
+
* @param {string} method - HTTP method.
|
|
177
|
+
* @param {string} path - API path (relative to baseUrl).
|
|
178
|
+
* @param {object} [options] - Request options.
|
|
179
|
+
* @param {object} [options.body] - Request body (will be JSON-stringified).
|
|
180
|
+
* @returns {Promise<any>} Parsed JSON response body.
|
|
181
|
+
* @throws {SqlCrudError} On HTTP errors.
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
async _request(method, path, options = {}) {
|
|
185
|
+
const url = `${this.baseUrl}${path}`;
|
|
186
|
+
const hasBody = options.body !== undefined;
|
|
187
|
+
const headers = this._buildHeaders(method, hasBody);
|
|
188
|
+
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
method,
|
|
191
|
+
headers,
|
|
192
|
+
body: hasBody ? JSON.stringify(options.body) : undefined,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const body = await response.json();
|
|
196
|
+
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const errorDetails = typeof body === 'object' && body !== null ? body : undefined;
|
|
199
|
+
|
|
200
|
+
if (response.status === 401) {
|
|
201
|
+
throw new SqlCrudAuthError(path, errorDetails);
|
|
202
|
+
}
|
|
203
|
+
if (response.status === 404) {
|
|
204
|
+
const model = decodeURIComponent(path.split('/').pop() ?? path);
|
|
205
|
+
throw new SqlCrudNotFoundError(model, path);
|
|
206
|
+
}
|
|
207
|
+
if (response.status >= 500) {
|
|
208
|
+
throw new SqlCrudServerError(path, response.status, errorDetails);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Other errors (400, etc.)
|
|
212
|
+
const message =
|
|
213
|
+
typeof body === 'object' && body?.error ? body.error : `HTTP ${response.status}`;
|
|
214
|
+
throw new SqlCrudError(message, {
|
|
215
|
+
statusCode: response.status,
|
|
216
|
+
endpoint: path,
|
|
217
|
+
details: errorDetails,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return body;
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all sqlcrud client errors.
|
|
3
|
+
* All library errors extend this class for easy instanceof checking.
|
|
4
|
+
*/
|
|
5
|
+
export class SqlCrudError extends Error {
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} message - The error message.
|
|
8
|
+
* @param {object} [options] - Optional error details.
|
|
9
|
+
* @param {number} [options.statusCode] - HTTP status code.
|
|
10
|
+
* @param {string} [options.endpoint] - The API endpoint that failed.
|
|
11
|
+
* @param {object} [options.details] - Additional error details from the server.
|
|
12
|
+
*/
|
|
13
|
+
constructor(message, options = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'SqlCrudError';
|
|
16
|
+
this.statusCode = options.statusCode;
|
|
17
|
+
this.endpoint = options.endpoint;
|
|
18
|
+
this.details = options.details;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when the server returns a 401 Unauthorized response.
|
|
24
|
+
*/
|
|
25
|
+
export class SqlCrudAuthError extends SqlCrudError {
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} endpoint - The API endpoint that failed.
|
|
28
|
+
* @param {object} [details] - Server error details.
|
|
29
|
+
*/
|
|
30
|
+
constructor(endpoint, details) {
|
|
31
|
+
super(`Authentication failed for ${endpoint}`, { statusCode: 401, endpoint, details });
|
|
32
|
+
this.name = 'SqlCrudAuthError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Thrown when the server returns a 404 Not Found response.
|
|
38
|
+
*/
|
|
39
|
+
export class SqlCrudNotFoundError extends SqlCrudError {
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} model - The model name that was not found.
|
|
42
|
+
* @param {string} endpoint - The API endpoint that failed.
|
|
43
|
+
*/
|
|
44
|
+
constructor(model, endpoint) {
|
|
45
|
+
super(`Model "${model}" not found at ${endpoint}`, { statusCode: 404, endpoint });
|
|
46
|
+
this.name = 'SqlCrudNotFoundError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when client-side validation fails (invalid schema, bad identifier, etc.).
|
|
52
|
+
*/
|
|
53
|
+
export class SqlCrudValidationError extends SqlCrudError {
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} message - The validation error message.
|
|
56
|
+
* @param {object} [details] - Additional validation details.
|
|
57
|
+
*/
|
|
58
|
+
constructor(message, details) {
|
|
59
|
+
super(`Validation error: ${message}`, { statusCode: 400, details });
|
|
60
|
+
this.name = 'SqlCrudValidationError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Thrown when the server returns a 5xx error response.
|
|
66
|
+
*/
|
|
67
|
+
export class SqlCrudServerError extends SqlCrudError {
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} endpoint - The API endpoint that failed.
|
|
70
|
+
* @param {number} statusCode - The HTTP 5xx status code.
|
|
71
|
+
* @param {object} [details] - Server error details.
|
|
72
|
+
*/
|
|
73
|
+
constructor(endpoint, statusCode, details) {
|
|
74
|
+
super(`Server error (${statusCode}) at ${endpoint}`, { statusCode, endpoint, details });
|
|
75
|
+
this.name = 'SqlCrudServerError';
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SqlCrudClient (main export)
|
|
2
|
+
export { SqlCrudClient } from './client.js';
|
|
3
|
+
|
|
4
|
+
// Error classes
|
|
5
|
+
export {
|
|
6
|
+
SqlCrudError,
|
|
7
|
+
SqlCrudAuthError,
|
|
8
|
+
SqlCrudNotFoundError,
|
|
9
|
+
SqlCrudValidationError,
|
|
10
|
+
SqlCrudServerError,
|
|
11
|
+
} from './errors.js';
|
|
12
|
+
|
|
13
|
+
// Utility functions and constants
|
|
14
|
+
export {
|
|
15
|
+
FIELD_TYPES,
|
|
16
|
+
RESERVED_FIELD_NAMES,
|
|
17
|
+
IDENTIFIER_PATTERN,
|
|
18
|
+
validateIdentifier,
|
|
19
|
+
validateFieldName,
|
|
20
|
+
validateSchema,
|
|
21
|
+
coerceBooleans,
|
|
22
|
+
buildQueryString,
|
|
23
|
+
} from './utils.js';
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { SqlCrudValidationError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Valid schema field types.
|
|
5
|
+
* @readonly
|
|
6
|
+
* @type {readonly ['string', 'integer', 'float', 'boolean', 'json', 'datetime', 'time']}
|
|
7
|
+
*/
|
|
8
|
+
export const FIELD_TYPES = ['string', 'integer', 'float', 'boolean', 'json', 'datetime', 'time'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Field names reserved by sqlcrud (cannot be used in user schemas).
|
|
12
|
+
* @readonly
|
|
13
|
+
* @type {readonly ['model', 'schema']}
|
|
14
|
+
*/
|
|
15
|
+
export const RESERVED_FIELD_NAMES = ['model', 'schema'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pattern for valid SQL identifiers (model and field names).
|
|
19
|
+
* @readonly
|
|
20
|
+
* @type {RegExp}
|
|
21
|
+
*/
|
|
22
|
+
export const IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validates that a name matches SQL identifier conventions.
|
|
26
|
+
* @param {string} name - The name to validate.
|
|
27
|
+
* @param {string} [label='Identifier'] - Human-readable label for error messages.
|
|
28
|
+
* @throws {SqlCrudValidationError}
|
|
29
|
+
*/
|
|
30
|
+
export function validateIdentifier(name, label = 'Identifier') {
|
|
31
|
+
if (typeof name !== 'string' || !IDENTIFIER_PATTERN.test(name)) {
|
|
32
|
+
throw new SqlCrudValidationError(
|
|
33
|
+
`${label} "${name}" must match ${IDENTIFIER_PATTERN.source}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validates a field name (identifier rules + reserved names).
|
|
40
|
+
* @param {string} name - The field name to validate.
|
|
41
|
+
* @throws {SqlCrudValidationError}
|
|
42
|
+
*/
|
|
43
|
+
export function validateFieldName(name) {
|
|
44
|
+
validateIdentifier(name, 'Field name');
|
|
45
|
+
if (RESERVED_FIELD_NAMES.includes(name)) {
|
|
46
|
+
throw new SqlCrudValidationError(`Field name "${name}" is reserved`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validates a complete schema definition object.
|
|
52
|
+
* @param {object} schema - The schema to validate.
|
|
53
|
+
* @throws {SqlCrudValidationError}
|
|
54
|
+
*/
|
|
55
|
+
export function validateSchema(schema) {
|
|
56
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
|
|
57
|
+
throw new SqlCrudValidationError('Schema must be a JSON object');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [fieldName, def] of Object.entries(schema)) {
|
|
61
|
+
validateFieldName(fieldName);
|
|
62
|
+
|
|
63
|
+
if (typeof def !== 'object' || def === null || Array.isArray(def)) {
|
|
64
|
+
throw new SqlCrudValidationError(`Field "${fieldName}" definition must be a non-null object`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!def.type || !FIELD_TYPES.includes(def.type)) {
|
|
68
|
+
throw new SqlCrudValidationError(
|
|
69
|
+
`Field "${fieldName}" must have a valid type from [${FIELD_TYPES.join(', ')}]`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (def.length !== undefined) {
|
|
74
|
+
if (def.type !== 'string') {
|
|
75
|
+
throw new SqlCrudValidationError(`Field "${fieldName}" length is only valid for string types`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof def.length !== 'number' || def.length < 1) {
|
|
78
|
+
throw new SqlCrudValidationError(`Field "${fieldName}" length must be a positive number`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const attr of ['unique', 'primary', 'notnull']) {
|
|
83
|
+
if (def[attr] !== undefined && typeof def[attr] !== 'boolean') {
|
|
84
|
+
throw new SqlCrudValidationError(`Field "${fieldName}" '${attr}' must be a boolean`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Coerces boolean values in a record to 0/1 (SQLite compatibility).
|
|
92
|
+
* Returns a new object; does not mutate the input.
|
|
93
|
+
* @param {object} record - The record data.
|
|
94
|
+
* @returns {object} A new object with booleans coerced.
|
|
95
|
+
*/
|
|
96
|
+
export function coerceBooleans(record) {
|
|
97
|
+
const result = {};
|
|
98
|
+
for (const [key, value] of Object.entries(record)) {
|
|
99
|
+
result[key] = typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Converts a params object into a URL query string fragment.
|
|
106
|
+
* Filters out null and undefined values.
|
|
107
|
+
* @param {object} params - Key-value pairs for query parameters.
|
|
108
|
+
* @returns {string} Query string starting with '?' or empty string.
|
|
109
|
+
*/
|
|
110
|
+
export function buildQueryString(params) {
|
|
111
|
+
const searchParams = new URLSearchParams();
|
|
112
|
+
for (const [key, value] of Object.entries(params)) {
|
|
113
|
+
if (value !== null && value !== undefined) {
|
|
114
|
+
searchParams.set(key, String(value));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const qs = searchParams.toString();
|
|
118
|
+
return qs ? `?${qs}` : '';
|
|
119
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// Types
|
|
3
|
+
// ─────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type FieldType =
|
|
6
|
+
| 'string'
|
|
7
|
+
| 'integer'
|
|
8
|
+
| 'float'
|
|
9
|
+
| 'boolean'
|
|
10
|
+
| 'json'
|
|
11
|
+
| 'datetime'
|
|
12
|
+
| 'time';
|
|
13
|
+
|
|
14
|
+
export interface FieldDefinition {
|
|
15
|
+
type: FieldType;
|
|
16
|
+
length?: number;
|
|
17
|
+
unique?: boolean;
|
|
18
|
+
primary?: boolean;
|
|
19
|
+
notnull?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SchemaDefinition = Record<string, FieldDefinition>;
|
|
23
|
+
|
|
24
|
+
export interface ModelInfo {
|
|
25
|
+
model: string;
|
|
26
|
+
schema: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RecordData = Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
export interface UpdateResponse {
|
|
32
|
+
before: RecordData;
|
|
33
|
+
after: RecordData;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type QueryParams = Record<
|
|
37
|
+
string,
|
|
38
|
+
string | number | boolean | null | undefined
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
export interface SqlCrudClientConfig {
|
|
42
|
+
baseUrl: string;
|
|
43
|
+
username?: string;
|
|
44
|
+
password?: string;
|
|
45
|
+
headers?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────
|
|
49
|
+
// Constants
|
|
50
|
+
// ─────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export const FIELD_TYPES: readonly FieldType[];
|
|
53
|
+
export const RESERVED_FIELD_NAMES: readonly ['model', 'schema'];
|
|
54
|
+
export const IDENTIFIER_PATTERN: RegExp;
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────────────────────────
|
|
57
|
+
// Utility Functions
|
|
58
|
+
// ─────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export function validateIdentifier(
|
|
61
|
+
name: string,
|
|
62
|
+
label?: string
|
|
63
|
+
): void;
|
|
64
|
+
|
|
65
|
+
export function validateFieldName(name: string): void;
|
|
66
|
+
|
|
67
|
+
export function validateSchema(schema: unknown): void;
|
|
68
|
+
|
|
69
|
+
export function coerceBooleans(record: RecordData): RecordData;
|
|
70
|
+
|
|
71
|
+
export function buildQueryString(params: QueryParams): string;
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────
|
|
74
|
+
// Errors
|
|
75
|
+
// ─────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export class SqlCrudError extends Error {
|
|
78
|
+
constructor(message: string, options?: {
|
|
79
|
+
statusCode?: number;
|
|
80
|
+
endpoint?: string;
|
|
81
|
+
details?: Record<string, unknown>;
|
|
82
|
+
});
|
|
83
|
+
name: 'SqlCrudError';
|
|
84
|
+
statusCode?: number;
|
|
85
|
+
endpoint?: string;
|
|
86
|
+
details?: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class SqlCrudAuthError extends SqlCrudError {
|
|
90
|
+
constructor(endpoint?: string, details?: Record<string, unknown>);
|
|
91
|
+
name: 'SqlCrudAuthError';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class SqlCrudNotFoundError extends SqlCrudError {
|
|
95
|
+
constructor(model: string, endpoint: string);
|
|
96
|
+
name: 'SqlCrudNotFoundError';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class SqlCrudValidationError extends SqlCrudError {
|
|
100
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
101
|
+
name: 'SqlCrudValidationError';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class SqlCrudServerError extends SqlCrudError {
|
|
105
|
+
constructor(
|
|
106
|
+
endpoint: string,
|
|
107
|
+
statusCode: number,
|
|
108
|
+
details?: Record<string, unknown>
|
|
109
|
+
);
|
|
110
|
+
name: 'SqlCrudServerError';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────
|
|
114
|
+
// SqlCrudClient
|
|
115
|
+
// ─────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export class SqlCrudClient {
|
|
118
|
+
constructor(config: SqlCrudClientConfig);
|
|
119
|
+
|
|
120
|
+
readonly baseUrl: string;
|
|
121
|
+
readonly authHeader: string | null;
|
|
122
|
+
readonly extraHeaders: Record<string, string>;
|
|
123
|
+
|
|
124
|
+
// Schema management
|
|
125
|
+
listTables(): Promise<string[]>;
|
|
126
|
+
listModels(): Promise<ModelInfo[]>;
|
|
127
|
+
getSchema(model: string): Promise<SchemaDefinition>;
|
|
128
|
+
setSchema(model: string, schema: SchemaDefinition): Promise<void>;
|
|
129
|
+
deleteSchema(model: string): Promise<void>;
|
|
130
|
+
|
|
131
|
+
// Record operations
|
|
132
|
+
queryRecords(
|
|
133
|
+
model: string,
|
|
134
|
+
params?: QueryParams
|
|
135
|
+
): Promise<RecordData[]>;
|
|
136
|
+
createRecord(model: string, data: RecordData): Promise<RecordData>;
|
|
137
|
+
updateRecord(
|
|
138
|
+
model: string,
|
|
139
|
+
criteria: QueryParams,
|
|
140
|
+
data: RecordData
|
|
141
|
+
): Promise<UpdateResponse>;
|
|
142
|
+
deleteRecord(model: string, criteria: QueryParams): Promise<void>;
|
|
143
|
+
}
|