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.
Files changed (115) hide show
  1. package/.dockerignore +10 -0
  2. package/.env.example +46 -0
  3. package/.github/workflows/browser-test.yml +34 -0
  4. package/.github/workflows/docker-publish.yml +54 -0
  5. package/.github/workflows/docs.yml +31 -0
  6. package/.github/workflows/npm-chadstart.yml +27 -0
  7. package/.github/workflows/npm-sdk.yml +38 -0
  8. package/.github/workflows/test.yml +85 -0
  9. package/.weblate +9 -0
  10. package/Dockerfile +23 -0
  11. package/README.md +348 -0
  12. package/admin/index.html +2802 -0
  13. package/admin/login.html +207 -0
  14. package/chadstart.example.yml +416 -0
  15. package/chadstart.schema.json +367 -0
  16. package/chadstart.yaml +53 -0
  17. package/cli/cli.js +295 -0
  18. package/core/api-generator.js +606 -0
  19. package/core/auth.js +298 -0
  20. package/core/db.js +384 -0
  21. package/core/entity-engine.js +166 -0
  22. package/core/error-reporter.js +132 -0
  23. package/core/file-storage.js +97 -0
  24. package/core/functions-engine.js +353 -0
  25. package/core/openapi.js +171 -0
  26. package/core/plugin-loader.js +92 -0
  27. package/core/realtime.js +93 -0
  28. package/core/schema-validator.js +50 -0
  29. package/core/seeder.js +231 -0
  30. package/core/telemetry.js +119 -0
  31. package/core/upload.js +372 -0
  32. package/core/workers/php_worker.php +19 -0
  33. package/core/workers/python_worker.py +33 -0
  34. package/core/workers/ruby_worker.rb +21 -0
  35. package/core/yaml-loader.js +64 -0
  36. package/demo/chadstart.yaml +178 -0
  37. package/demo/docker-compose.yml +31 -0
  38. package/demo/functions/greet.go +39 -0
  39. package/demo/functions/hello.cpp +18 -0
  40. package/demo/functions/hello.py +13 -0
  41. package/demo/functions/hello.rb +10 -0
  42. package/demo/functions/onTodoCreated.js +13 -0
  43. package/demo/functions/ping.sh +13 -0
  44. package/demo/functions/stats.js +22 -0
  45. package/demo/public/index.html +522 -0
  46. package/docker-compose.yml +17 -0
  47. package/docs/access-policies.md +155 -0
  48. package/docs/admin-ui.md +29 -0
  49. package/docs/angular.md +69 -0
  50. package/docs/astro.md +71 -0
  51. package/docs/auth.md +160 -0
  52. package/docs/cli.md +56 -0
  53. package/docs/config.md +127 -0
  54. package/docs/crud.md +627 -0
  55. package/docs/deploy.md +113 -0
  56. package/docs/docker.md +59 -0
  57. package/docs/entities.md +385 -0
  58. package/docs/functions.md +196 -0
  59. package/docs/getting-started.md +79 -0
  60. package/docs/groups.md +85 -0
  61. package/docs/index.md +5 -0
  62. package/docs/llm-rules.md +81 -0
  63. package/docs/middlewares.md +78 -0
  64. package/docs/overrides/home.html +350 -0
  65. package/docs/plugins.md +59 -0
  66. package/docs/react.md +75 -0
  67. package/docs/realtime.md +43 -0
  68. package/docs/s3-storage.md +40 -0
  69. package/docs/security.md +23 -0
  70. package/docs/stylesheets/extra.css +375 -0
  71. package/docs/svelte.md +71 -0
  72. package/docs/telemetry.md +97 -0
  73. package/docs/upload.md +168 -0
  74. package/docs/validation.md +115 -0
  75. package/docs/vue.md +86 -0
  76. package/docs/webhooks.md +87 -0
  77. package/index.js +11 -0
  78. package/locales/en/admin.json +169 -0
  79. package/mkdocs.yml +82 -0
  80. package/package.json +65 -0
  81. package/playwright.config.js +24 -0
  82. package/public/.gitkeep +0 -0
  83. package/sdk/README.md +284 -0
  84. package/sdk/package.json +39 -0
  85. package/sdk/scripts/build.js +58 -0
  86. package/sdk/src/index.js +368 -0
  87. package/sdk/test/sdk.test.cjs +340 -0
  88. package/sdk/types/index.d.ts +217 -0
  89. package/server/express-server.js +734 -0
  90. package/test/access-policies.test.js +96 -0
  91. package/test/ai.test.js +81 -0
  92. package/test/api-keys.test.js +361 -0
  93. package/test/auth.test.js +122 -0
  94. package/test/browser/admin-ui.spec.js +127 -0
  95. package/test/browser/global-setup.js +71 -0
  96. package/test/browser/global-teardown.js +11 -0
  97. package/test/db.test.js +227 -0
  98. package/test/entity-engine.test.js +193 -0
  99. package/test/error-reporter.test.js +140 -0
  100. package/test/functions-engine.test.js +240 -0
  101. package/test/groups.test.js +212 -0
  102. package/test/hot-reload.test.js +153 -0
  103. package/test/i18n.test.js +173 -0
  104. package/test/middleware.test.js +76 -0
  105. package/test/openapi.test.js +67 -0
  106. package/test/schema-validator.test.js +83 -0
  107. package/test/sdk.test.js +90 -0
  108. package/test/seeder.test.js +279 -0
  109. package/test/settings.test.js +109 -0
  110. package/test/telemetry.test.js +254 -0
  111. package/test/test.js +17 -0
  112. package/test/upload.test.js +265 -0
  113. package/test/validation.test.js +96 -0
  114. package/test/yaml-loader.test.js +93 -0
  115. package/utils/logger.js +24 -0
@@ -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;