chadstart 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/.dockerignore +10 -0
- package/.env.example +46 -0
- package/.github/workflows/browser-test.yml +34 -0
- package/.github/workflows/docker-publish.yml +54 -0
- package/.github/workflows/docs.yml +31 -0
- package/.github/workflows/npm-chadstart.yml +27 -0
- package/.github/workflows/npm-sdk.yml +38 -0
- package/.github/workflows/test.yml +85 -0
- package/.weblate +9 -0
- package/Dockerfile +23 -0
- package/README.md +348 -0
- package/admin/index.html +2802 -0
- package/admin/login.html +207 -0
- package/chadstart.example.yml +416 -0
- package/chadstart.schema.json +367 -0
- package/chadstart.yaml +53 -0
- package/cli/cli.js +295 -0
- package/core/api-generator.js +606 -0
- package/core/auth.js +298 -0
- package/core/db.js +384 -0
- package/core/entity-engine.js +166 -0
- package/core/error-reporter.js +132 -0
- package/core/file-storage.js +97 -0
- package/core/functions-engine.js +353 -0
- package/core/openapi.js +171 -0
- package/core/plugin-loader.js +92 -0
- package/core/realtime.js +93 -0
- package/core/schema-validator.js +50 -0
- package/core/seeder.js +231 -0
- package/core/telemetry.js +119 -0
- package/core/upload.js +372 -0
- package/core/workers/php_worker.php +19 -0
- package/core/workers/python_worker.py +33 -0
- package/core/workers/ruby_worker.rb +21 -0
- package/core/yaml-loader.js +64 -0
- package/demo/chadstart.yaml +178 -0
- package/demo/docker-compose.yml +31 -0
- package/demo/functions/greet.go +39 -0
- package/demo/functions/hello.cpp +18 -0
- package/demo/functions/hello.py +13 -0
- package/demo/functions/hello.rb +10 -0
- package/demo/functions/onTodoCreated.js +13 -0
- package/demo/functions/ping.sh +13 -0
- package/demo/functions/stats.js +22 -0
- package/demo/public/index.html +522 -0
- package/docker-compose.yml +17 -0
- package/docs/access-policies.md +155 -0
- package/docs/admin-ui.md +29 -0
- package/docs/angular.md +69 -0
- package/docs/astro.md +71 -0
- package/docs/auth.md +160 -0
- package/docs/cli.md +56 -0
- package/docs/config.md +127 -0
- package/docs/crud.md +627 -0
- package/docs/deploy.md +113 -0
- package/docs/docker.md +59 -0
- package/docs/entities.md +385 -0
- package/docs/functions.md +196 -0
- package/docs/getting-started.md +79 -0
- package/docs/groups.md +85 -0
- package/docs/index.md +5 -0
- package/docs/llm-rules.md +81 -0
- package/docs/middlewares.md +78 -0
- package/docs/overrides/home.html +350 -0
- package/docs/plugins.md +59 -0
- package/docs/react.md +75 -0
- package/docs/realtime.md +43 -0
- package/docs/s3-storage.md +40 -0
- package/docs/security.md +23 -0
- package/docs/stylesheets/extra.css +375 -0
- package/docs/svelte.md +71 -0
- package/docs/telemetry.md +97 -0
- package/docs/upload.md +168 -0
- package/docs/validation.md +115 -0
- package/docs/vue.md +86 -0
- package/docs/webhooks.md +87 -0
- package/index.js +11 -0
- package/locales/en/admin.json +169 -0
- package/mkdocs.yml +82 -0
- package/package.json +65 -0
- package/playwright.config.js +24 -0
- package/public/.gitkeep +0 -0
- package/sdk/README.md +284 -0
- package/sdk/package.json +39 -0
- package/sdk/scripts/build.js +58 -0
- package/sdk/src/index.js +368 -0
- package/sdk/test/sdk.test.cjs +340 -0
- package/sdk/types/index.d.ts +217 -0
- package/server/express-server.js +734 -0
- package/test/access-policies.test.js +96 -0
- package/test/ai.test.js +81 -0
- package/test/api-keys.test.js +361 -0
- package/test/auth.test.js +122 -0
- package/test/browser/admin-ui.spec.js +127 -0
- package/test/browser/global-setup.js +71 -0
- package/test/browser/global-teardown.js +11 -0
- package/test/db.test.js +227 -0
- package/test/entity-engine.test.js +193 -0
- package/test/error-reporter.test.js +140 -0
- package/test/functions-engine.test.js +240 -0
- package/test/groups.test.js +212 -0
- package/test/hot-reload.test.js +153 -0
- package/test/i18n.test.js +173 -0
- package/test/middleware.test.js +76 -0
- package/test/openapi.test.js +67 -0
- package/test/schema-validator.test.js +83 -0
- package/test/sdk.test.js +90 -0
- package/test/seeder.test.js +279 -0
- package/test/settings.test.js +109 -0
- package/test/telemetry.test.js +254 -0
- package/test/test.js +17 -0
- package/test/upload.test.js +265 -0
- package/test/validation.test.js +96 -0
- package/test/yaml-loader.test.js +93 -0
- package/utils/logger.js +24 -0
package/sdk/src/index.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chadstart JavaScript SDK
|
|
3
|
+
*
|
|
4
|
+
* Works with vanilla JS and TypeScript projects (browser + Node.js 18+).
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import Chadstart from '@chadstart/sdk'
|
|
8
|
+
* const client = new Chadstart('http://localhost:3000')
|
|
9
|
+
*
|
|
10
|
+
* // Collections
|
|
11
|
+
* const posts = await client.from('posts').find()
|
|
12
|
+
* const post = await client.from('posts').findOneById(id)
|
|
13
|
+
* const post = await client.from('posts').create({ title: 'Hello' })
|
|
14
|
+
* const post = await client.from('posts').update(id, { title: 'Hi' })
|
|
15
|
+
* const post = await client.from('posts').patch(id, { title: 'Hi' })
|
|
16
|
+
* const post = await client.from('posts').delete(id)
|
|
17
|
+
*
|
|
18
|
+
* // Singles
|
|
19
|
+
* const page = await client.single('homepage').get()
|
|
20
|
+
* const page = await client.single('homepage').update({ title: 'New' })
|
|
21
|
+
* const page = await client.single('homepage').patch({ title: 'New' })
|
|
22
|
+
*
|
|
23
|
+
* // Auth
|
|
24
|
+
* const { token, user } = await client.auth('customers').signup({ email, password })
|
|
25
|
+
* const { token, user } = await client.auth('customers').login({ email, password })
|
|
26
|
+
* const user = await client.auth('customers').me()
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
class CollectionQuery {
|
|
30
|
+
constructor(slug, client) {
|
|
31
|
+
this._slug = slug;
|
|
32
|
+
this._client = client;
|
|
33
|
+
this._filters = [];
|
|
34
|
+
this._relations = [];
|
|
35
|
+
this._orderByField = null;
|
|
36
|
+
this._orderDir = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add a filter condition.
|
|
41
|
+
* Supports operators: =, !=, >, >=, <, <=, like, in
|
|
42
|
+
*
|
|
43
|
+
* @param {string} condition - e.g. 'published = true', 'age >= 18', 'name like %jo%'
|
|
44
|
+
*/
|
|
45
|
+
where(condition) {
|
|
46
|
+
this._filters.push(condition);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Alias for chaining additional filters. */
|
|
51
|
+
andWhere(condition) {
|
|
52
|
+
return this.where(condition);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load relations.
|
|
57
|
+
* @param {string[]} relations - e.g. ['author', 'tags', 'author.profile']
|
|
58
|
+
*/
|
|
59
|
+
with(relations) {
|
|
60
|
+
this._relations = Array.isArray(relations) ? relations : [relations];
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Order results.
|
|
66
|
+
* @param {string} field - property name
|
|
67
|
+
* @param {{ desc?: boolean }} [options]
|
|
68
|
+
*/
|
|
69
|
+
orderBy(field, options) {
|
|
70
|
+
this._orderByField = field;
|
|
71
|
+
this._orderDir = options && options.desc ? 'DESC' : 'ASC';
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fetch paginated list.
|
|
77
|
+
* @param {{ page?: number, perPage?: number }} [params]
|
|
78
|
+
* @returns {Promise<{ data: object[], currentPage: number, lastPage: number, from: number, to: number, total: number, perPage: number }>}
|
|
79
|
+
*/
|
|
80
|
+
async find(params) {
|
|
81
|
+
const query = this._buildQuery(params);
|
|
82
|
+
return this._client._request('GET', `/api/collections/${this._slug}${query}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetch a single item by ID.
|
|
87
|
+
* @param {string} id
|
|
88
|
+
*/
|
|
89
|
+
async findOneById(id) {
|
|
90
|
+
const parts = [];
|
|
91
|
+
if (this._relations.length) parts.push(`relations=${this._relations.join(',')}`);
|
|
92
|
+
const query = parts.length ? `?${parts.join('&')}` : '';
|
|
93
|
+
return this._client._request('GET', `/api/collections/${this._slug}/${id}${query}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a new item.
|
|
98
|
+
* @param {object} data
|
|
99
|
+
*/
|
|
100
|
+
async create(data) {
|
|
101
|
+
return this._client._request('POST', `/api/collections/${this._slug}`, data);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fully replace an item (PUT).
|
|
106
|
+
* @param {string} id
|
|
107
|
+
* @param {object} data
|
|
108
|
+
*/
|
|
109
|
+
async update(id, data) {
|
|
110
|
+
return this._client._request('PUT', `/api/collections/${this._slug}/${id}`, data);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Partially update an item (PATCH).
|
|
115
|
+
* @param {string} id
|
|
116
|
+
* @param {object} data
|
|
117
|
+
*/
|
|
118
|
+
async patch(id, data) {
|
|
119
|
+
return this._client._request('PATCH', `/api/collections/${this._slug}/${id}`, data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Delete an item.
|
|
124
|
+
* @param {string} id
|
|
125
|
+
*/
|
|
126
|
+
async delete(id) {
|
|
127
|
+
return this._client._request('DELETE', `/api/collections/${this._slug}/${id}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @private Build query string from current builder state + pagination params. */
|
|
131
|
+
_buildQuery(params) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
|
|
134
|
+
for (const condition of this._filters) {
|
|
135
|
+
const param = parseFilter(condition);
|
|
136
|
+
if (param) parts.push(param);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this._relations.length) {
|
|
140
|
+
parts.push(`relations=${this._relations.join(',')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this._orderByField) {
|
|
144
|
+
parts.push(`orderBy=${encodeURIComponent(this._orderByField)}`);
|
|
145
|
+
if (this._orderDir) parts.push(`order=${this._orderDir}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (params) {
|
|
149
|
+
if (params.page != null) parts.push(`page=${params.page}`);
|
|
150
|
+
if (params.perPage != null) parts.push(`perPage=${params.perPage}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
class SingleQuery {
|
|
158
|
+
constructor(slug, client) {
|
|
159
|
+
this._slug = slug;
|
|
160
|
+
this._client = client;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Fetch the single entity. */
|
|
164
|
+
async get() {
|
|
165
|
+
return this._client._request('GET', `/api/singles/${this._slug}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Fully replace the single entity (PUT).
|
|
170
|
+
* @param {object} data
|
|
171
|
+
*/
|
|
172
|
+
async update(data) {
|
|
173
|
+
return this._client._request('PUT', `/api/singles/${this._slug}`, data);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Partially update the single entity (PATCH).
|
|
178
|
+
* @param {object} data
|
|
179
|
+
*/
|
|
180
|
+
async patch(data) {
|
|
181
|
+
return this._client._request('PATCH', `/api/singles/${this._slug}`, data);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class AuthQuery {
|
|
186
|
+
constructor(slug, client) {
|
|
187
|
+
this._slug = slug;
|
|
188
|
+
this._client = client;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Register a new user.
|
|
193
|
+
* @param {{ email: string, password: string, [key: string]: any }} data
|
|
194
|
+
* @returns {Promise<{ token: string, user: object }>}
|
|
195
|
+
*/
|
|
196
|
+
async signup(data) {
|
|
197
|
+
const result = await this._client._request('POST', `/api/auth/${this._slug}/signup`, data);
|
|
198
|
+
if (result.token) this._client.setToken(result.token);
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Log in an existing user.
|
|
204
|
+
* @param {{ email: string, password: string }} data
|
|
205
|
+
* @returns {Promise<{ token: string, user: object }>}
|
|
206
|
+
*/
|
|
207
|
+
async login(data) {
|
|
208
|
+
const result = await this._client._request('POST', `/api/auth/${this._slug}/login`, data);
|
|
209
|
+
if (result.token) this._client.setToken(result.token);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get current authenticated user.
|
|
215
|
+
* @returns {Promise<object>}
|
|
216
|
+
*/
|
|
217
|
+
async me() {
|
|
218
|
+
return this._client._request('GET', `/api/auth/${this._slug}/me`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Log out by clearing the stored token. */
|
|
222
|
+
logout() {
|
|
223
|
+
this._client.clearToken();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Chadstart SDK client.
|
|
229
|
+
*/
|
|
230
|
+
class Chadstart {
|
|
231
|
+
/**
|
|
232
|
+
* @param {string} [baseUrl='http://localhost:3000'] - The base URL of your Chadstart backend.
|
|
233
|
+
*/
|
|
234
|
+
constructor(baseUrl) {
|
|
235
|
+
this._baseUrl = (baseUrl || 'http://localhost:3000').replace(/\/$/, '');
|
|
236
|
+
this._token = null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Start a collection query builder.
|
|
241
|
+
* @param {string} slug - Collection slug (e.g. 'posts')
|
|
242
|
+
* @returns {CollectionQuery}
|
|
243
|
+
*/
|
|
244
|
+
from(slug) {
|
|
245
|
+
return new CollectionQuery(slug, this);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Start a single entity query builder.
|
|
250
|
+
* @param {string} slug - Single slug (e.g. 'homepage')
|
|
251
|
+
* @returns {SingleQuery}
|
|
252
|
+
*/
|
|
253
|
+
single(slug) {
|
|
254
|
+
return new SingleQuery(slug, this);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Start an auth query builder.
|
|
259
|
+
* @param {string} slug - Authenticable entity slug (e.g. 'customers')
|
|
260
|
+
* @returns {AuthQuery}
|
|
261
|
+
*/
|
|
262
|
+
auth(slug) {
|
|
263
|
+
return new AuthQuery(slug, this);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Set the Bearer token used for authenticated requests.
|
|
268
|
+
* @param {string} token
|
|
269
|
+
*/
|
|
270
|
+
setToken(token) {
|
|
271
|
+
this._token = token;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Clear the stored token (logout). */
|
|
275
|
+
clearToken() {
|
|
276
|
+
this._token = null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** @private Perform an HTTP request. */
|
|
280
|
+
async _request(method, path, body) {
|
|
281
|
+
const url = `${this._baseUrl}${path}`;
|
|
282
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
283
|
+
if (this._token) headers['Authorization'] = `Bearer ${this._token}`;
|
|
284
|
+
|
|
285
|
+
const options = { method, headers };
|
|
286
|
+
if (body !== undefined && method !== 'GET' && method !== 'DELETE') {
|
|
287
|
+
options.body = JSON.stringify(body);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const response = await fetch(url, options);
|
|
291
|
+
|
|
292
|
+
let data;
|
|
293
|
+
const contentType = response.headers.get('content-type') || '';
|
|
294
|
+
if (contentType.includes('application/json')) {
|
|
295
|
+
data = await response.json();
|
|
296
|
+
} else {
|
|
297
|
+
data = await response.text();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
const message =
|
|
302
|
+
(data && typeof data === 'object' && data.error) ||
|
|
303
|
+
(typeof data === 'string' && data) ||
|
|
304
|
+
`HTTP ${response.status}`;
|
|
305
|
+
const error = new ChadstartError(message, response.status, data);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return data;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Error thrown when the Chadstart API returns a non-2xx status.
|
|
315
|
+
*/
|
|
316
|
+
class ChadstartError extends Error {
|
|
317
|
+
/**
|
|
318
|
+
* @param {string} message
|
|
319
|
+
* @param {number} status - HTTP status code
|
|
320
|
+
* @param {any} data - Raw response body
|
|
321
|
+
*/
|
|
322
|
+
constructor(message, status, data) {
|
|
323
|
+
super(message);
|
|
324
|
+
this.name = 'ChadstartError';
|
|
325
|
+
this.status = status;
|
|
326
|
+
this.data = data;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Parse a filter condition string into a query parameter string.
|
|
332
|
+
*
|
|
333
|
+
* Supported operators: =, !=, >=, <=, >, <, like, in
|
|
334
|
+
*
|
|
335
|
+
* Examples:
|
|
336
|
+
* 'published = true' -> 'published_eq=true'
|
|
337
|
+
* 'age >= 18' -> 'age_gte=18'
|
|
338
|
+
* 'name like %jo%' -> 'name_like=%jo%'
|
|
339
|
+
* 'id in 1,2,3' -> 'id_in=1%2C2%2C3'
|
|
340
|
+
*
|
|
341
|
+
* @param {string} condition
|
|
342
|
+
* @returns {string|null}
|
|
343
|
+
*/
|
|
344
|
+
function parseFilter(condition) {
|
|
345
|
+
const operators = [
|
|
346
|
+
{ op: '!=', suffix: '_neq' },
|
|
347
|
+
{ op: '>=', suffix: '_gte' },
|
|
348
|
+
{ op: '<=', suffix: '_lte' },
|
|
349
|
+
{ op: '>', suffix: '_gt' },
|
|
350
|
+
{ op: '<', suffix: '_lt' },
|
|
351
|
+
{ op: '=', suffix: '_eq' },
|
|
352
|
+
{ op: ' like ', suffix: '_like' },
|
|
353
|
+
{ op: ' in ', suffix: '_in' },
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
for (const { op, suffix } of operators) {
|
|
357
|
+
const idx = condition.indexOf(op);
|
|
358
|
+
if (idx === -1) continue;
|
|
359
|
+
const field = condition.slice(0, idx).trim();
|
|
360
|
+
const value = condition.slice(idx + op.length).trim();
|
|
361
|
+
return `${encodeURIComponent(field)}${suffix}=${encodeURIComponent(value)}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export { Chadstart, ChadstartError, CollectionQuery, SingleQuery, AuthQuery };
|
|
368
|
+
export default Chadstart;
|