chadstart 1.0.0 → 1.0.2

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.
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "chadstart",
3
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:24-trixie",
4
+ "features": {},
5
+ "onCreateCommand": "sudo apt-get update && sudo apt-get install -y python3 make g++ curl wget && npm ci",
6
+ "postCreateCommand": "cp -n .env.example .env || true",
7
+ "forwardPorts": [3000],
8
+ "portsAttributes": {
9
+ "3000": {
10
+ "label": "chadstart API",
11
+ "onAutoForward": "notify"
12
+ }
13
+ },
14
+ "customizations": {
15
+ "vscode": {
16
+ "extensions": [
17
+ "dbaeumer.vscode-eslint",
18
+ "esbenp.prettier-vscode",
19
+ "redhat.vscode-yaml",
20
+ "humao.rest-client",
21
+ "rangav.vscode-thunder-client",
22
+ "pkief.material-icon-theme"
23
+ ],
24
+ "settings": {
25
+ "editor.formatOnSave": true,
26
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
27
+ "files.eol": "\n"
28
+ }
29
+ }
30
+ },
31
+ "remoteEnv": {
32
+ "NODE_ENV": "development"
33
+ }
34
+ }
package/.env.example CHANGED
@@ -14,7 +14,16 @@ TOKEN_SECRET_KEY=replace-with-a-long-random-secret
14
14
  # CHADSTART_FUNCTIONS_FOLDER=functions
15
15
 
16
16
  # Database (SQLite default)
17
- # DB_PATH=data/chadstart.db
17
+ # DB_ENGINE=sqlite # sqlite (default), postgres, or mysql
18
+ # DB_PATH=/data/chadstart.db # SQLite: path to database file
19
+
20
+ # PostgreSQL / MySQL / MariaDB (set DB_ENGINE=postgres or mysql to activate)
21
+ # DB_HOST=localhost
22
+ # DB_PORT=5432 # 5432 for PostgreSQL, 3306 for MySQL/MariaDB
23
+ # DB_USERNAME=postgres
24
+ # DB_PASSWORD=postgres
25
+ # DB_DATABASE=manifest
26
+ # DB_SSL=false # Set to true for remote managed databases
18
27
 
19
28
  # Optional: S3-compatible storage (when set, files are stored in S3 instead of the local filesystem)
20
29
  # S3_BUCKET=my-bucket-name
@@ -44,3 +53,23 @@ TOKEN_SECRET_KEY=replace-with-a-long-random-secret
44
53
  # 💡 Bugsink (https://www.bugsink.com) is a self-hosted alternative to Sentry
45
54
  # that uses the same Sentry SDK — just point SENTRY_DSN at your Bugsink instance.
46
55
  # SENTRY_DSN=https://xxxxx@oXXXXX.ingest.sentry.io/XXXXXXX
56
+
57
+ # Optional: OAuth / Social Login (powered by grant)
58
+ # For each provider, set OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET.
59
+ # Provider names must be uppercase (e.g. GOOGLE, GITHUB, FACEBOOK).
60
+ # See docs/oauth.md for the full list of 200+ supported providers.
61
+ #
62
+ # OAUTH_GOOGLE_KEY=your-google-client-id
63
+ # OAUTH_GOOGLE_SECRET=your-google-client-secret
64
+ # OAUTH_GITHUB_KEY=your-github-client-id
65
+ # OAUTH_GITHUB_SECRET=your-github-client-secret
66
+ # OAUTH_FACEBOOK_KEY=your-facebook-app-id
67
+ # OAUTH_FACEBOOK_SECRET=your-facebook-app-secret
68
+ # OAUTH_DISCORD_KEY=your-discord-client-id
69
+ # OAUTH_DISCORD_SECRET=your-discord-client-secret
70
+ # OAUTH_APPLE_KEY=your-apple-client-id
71
+ # OAUTH_APPLE_SECRET=your-apple-client-secret
72
+ # OAUTH_MICROSOFT_KEY=your-microsoft-client-id
73
+ # OAUTH_MICROSOFT_SECRET=your-microsoft-client-secret
74
+ # OAUTH_TWITTER_KEY=your-twitter-api-key
75
+ # OAUTH_TWITTER_SECRET=your-twitter-api-secret
@@ -0,0 +1,139 @@
1
+ name: DB Integration Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ # ── PostgreSQL ──────────────────────────────────────────────────────────────
10
+ test-postgres:
11
+ name: Integration – PostgreSQL
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: read
15
+
16
+ services:
17
+ postgres:
18
+ image: postgres:16
19
+ env:
20
+ POSTGRES_PASSWORD: postgres
21
+ POSTGRES_DB: chadstart_test
22
+ ports:
23
+ - 5432:5432
24
+ options: >-
25
+ --health-cmd pg_isready
26
+ --health-interval 10s
27
+ --health-timeout 5s
28
+ --health-retries 5
29
+
30
+ steps:
31
+ - name: Checkout
32
+ uses: actions/checkout@v4
33
+
34
+ - name: Set up Node.js
35
+ uses: actions/setup-node@v4
36
+ with:
37
+ node-version: "lts/*"
38
+ cache: "npm"
39
+
40
+ - name: Install dependencies
41
+ run: npm ci
42
+
43
+ - name: Run integration tests
44
+ run: npm run test:integration
45
+ env:
46
+ DB_ENGINE: postgres
47
+ DB_HOST: localhost
48
+ DB_PORT: 5432
49
+ DB_USERNAME: postgres
50
+ DB_PASSWORD: postgres
51
+ DB_DATABASE: chadstart_test
52
+
53
+ # ── MySQL ───────────────────────────────────────────────────────────────────
54
+ test-mysql:
55
+ name: Integration – MySQL
56
+ runs-on: ubuntu-latest
57
+ permissions:
58
+ contents: read
59
+
60
+ services:
61
+ mysql:
62
+ image: mysql:8
63
+ env:
64
+ MYSQL_ROOT_PASSWORD: root
65
+ MYSQL_DATABASE: chadstart_test
66
+ ports:
67
+ - 3306:3306
68
+ options: >-
69
+ --health-cmd "mysqladmin ping -h localhost -u root -proot"
70
+ --health-interval 10s
71
+ --health-timeout 5s
72
+ --health-retries 10
73
+
74
+ steps:
75
+ - name: Checkout
76
+ uses: actions/checkout@v4
77
+
78
+ - name: Set up Node.js
79
+ uses: actions/setup-node@v4
80
+ with:
81
+ node-version: "lts/*"
82
+ cache: "npm"
83
+
84
+ - name: Install dependencies
85
+ run: npm ci
86
+
87
+ - name: Run integration tests
88
+ run: npm run test:integration
89
+ env:
90
+ DB_ENGINE: mysql
91
+ DB_HOST: 127.0.0.1
92
+ DB_PORT: 3306
93
+ DB_USERNAME: root
94
+ DB_PASSWORD: root
95
+ DB_DATABASE: chadstart_test
96
+
97
+ # ── MariaDB ─────────────────────────────────────────────────────────────────
98
+ test-mariadb:
99
+ name: Integration – MariaDB
100
+ runs-on: ubuntu-latest
101
+ permissions:
102
+ contents: read
103
+
104
+ services:
105
+ mariadb:
106
+ image: mariadb:11
107
+ env:
108
+ MARIADB_ROOT_PASSWORD: root
109
+ MARIADB_DATABASE: chadstart_test
110
+ ports:
111
+ - 3306:3306
112
+ options: >-
113
+ --health-cmd "healthcheck.sh --connect --innodb_initialized"
114
+ --health-interval 10s
115
+ --health-timeout 5s
116
+ --health-retries 10
117
+
118
+ steps:
119
+ - name: Checkout
120
+ uses: actions/checkout@v4
121
+
122
+ - name: Set up Node.js
123
+ uses: actions/setup-node@v4
124
+ with:
125
+ node-version: "lts/*"
126
+ cache: "npm"
127
+
128
+ - name: Install dependencies
129
+ run: npm ci
130
+
131
+ - name: Run integration tests
132
+ run: npm run test:integration
133
+ env:
134
+ DB_ENGINE: mysql
135
+ DB_HOST: 127.0.0.1
136
+ DB_PORT: 3306
137
+ DB_USERNAME: root
138
+ DB_PASSWORD: root
139
+ DB_DATABASE: chadstart_test
@@ -9,7 +9,7 @@ jobs:
9
9
  name: NPM Publish
10
10
  runs-on: ubuntu-latest
11
11
  permissions:
12
- contents: read
12
+ contents: write
13
13
  id-token: write # required for npm provenance
14
14
  steps:
15
15
  - name: Checkout repository
@@ -21,6 +21,17 @@ jobs:
21
21
  node-version: '24'
22
22
  registry-url: 'https://registry.npmjs.org'
23
23
 
24
+ - name: Configure git
25
+ run: |
26
+ git config user.name "github-actions[bot]"
27
+ git config user.email "github-actions[bot]@users.noreply.github.com"
28
+
29
+ - name: Bump patch version
30
+ run: npm version patch --message "v%s [skip ci]"
31
+
32
+ - name: Push version bump
33
+ run: git push --follow-tags
34
+
24
35
  - name: Publish to npm
25
36
  run: npm publish --access public --provenance
26
37
  env:
@@ -14,7 +14,7 @@ jobs:
14
14
  run:
15
15
  working-directory: sdk
16
16
  permissions:
17
- contents: read
17
+ contents: write
18
18
  id-token: write # required for npm provenance
19
19
  steps:
20
20
  - name: Checkout repository
@@ -32,6 +32,17 @@ jobs:
32
32
  - name: Run SDK tests
33
33
  run: node test/sdk.test.cjs
34
34
 
35
+ - name: Configure git
36
+ run: |
37
+ git config user.name "github-actions[bot]"
38
+ git config user.email "github-actions[bot]@users.noreply.github.com"
39
+
40
+ - name: Bump patch version
41
+ run: npm version patch --message "v%s [skip ci]"
42
+
43
+ - name: Push version bump
44
+ run: git push --follow-tags
45
+
35
46
  - name: Publish to npm
36
47
  run: npm publish --access public --provenance
37
48
  env:
@@ -414,3 +414,55 @@ sentry:
414
414
  environment: production # Label sent to Sentry. Defaults to NODE_ENV.
415
415
  tracesSampleRate: 1.0 # Fraction of transactions to sample (0.0–1.0)
416
416
  debug: false # Enable Sentry SDK debug logging
417
+
418
+ # ── OAuth / Social Login ─────────────────────────────────────────────────────
419
+ # Powered by the "grant" library — supports 200+ OAuth providers.
420
+ # Secrets (client keys / secrets) MUST be set via environment variables:
421
+ # OAUTH_<PROVIDER>_KEY — client / app ID
422
+ # OAUTH_<PROVIDER>_SECRET — client / app secret
423
+ # See docs/oauth.md for the full provider list and setup guides.
424
+
425
+ oauth:
426
+ # Which authenticable entity to create/find users in (default: first authenticable entity).
427
+ entity: User
428
+
429
+ # Where to redirect after successful login. The JWT token is appended as ?token=...
430
+ # If omitted, the callback returns JSON instead.
431
+ successRedirect: /login?success=true
432
+
433
+ # Where to redirect on error. The error message is appended as ?error=...
434
+ errorRedirect: /login?error=true
435
+
436
+ # Default settings applied to all providers.
437
+ defaults:
438
+ transport: querystring
439
+
440
+ # Configure each provider you want to support.
441
+ # The provider names must match grant's provider names (lowercase).
442
+ # Full list: https://github.com/simov/grant#200-supported-providers
443
+ providers:
444
+ google:
445
+ scope:
446
+ - openid
447
+ - email
448
+ - profile
449
+ custom_params:
450
+ access_type: offline
451
+ # key and secret via: OAUTH_GOOGLE_KEY, OAUTH_GOOGLE_SECRET
452
+
453
+ github:
454
+ scope:
455
+ - user:email
456
+ # key and secret via: OAUTH_GITHUB_KEY, OAUTH_GITHUB_SECRET
457
+
458
+ # facebook:
459
+ # scope:
460
+ # - email
461
+ # - public_profile
462
+ # key and secret via: OAUTH_FACEBOOK_KEY, OAUTH_FACEBOOK_SECRET
463
+
464
+ # discord:
465
+ # scope:
466
+ # - identify
467
+ # - email
468
+ # key and secret via: OAUTH_DISCORD_KEY, OAUTH_DISCORD_SECRET
@@ -124,6 +124,43 @@
124
124
  "description": "Enable Sentry SDK debug logging."
125
125
  }
126
126
  }
127
+ },
128
+ "oauth": {
129
+ "type": "object",
130
+ "description": "OAuth / social login configuration powered by the grant library. Secrets (client keys and secrets) must be supplied via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
131
+ "additionalProperties": false,
132
+ "properties": {
133
+ "entity": {
134
+ "type": "string",
135
+ "description": "Name of the authenticable entity to use for OAuth users (e.g. 'User'). Defaults to the first authenticable entity."
136
+ },
137
+ "successRedirect": {
138
+ "type": "string",
139
+ "description": "URL to redirect to after successful OAuth login. The JWT token is appended as a ?token= query parameter. If omitted, returns JSON."
140
+ },
141
+ "errorRedirect": {
142
+ "type": "string",
143
+ "description": "URL to redirect to on OAuth error. The error message is appended as an ?error= query parameter. If omitted, returns JSON error."
144
+ },
145
+ "defaults": {
146
+ "type": "object",
147
+ "description": "Default settings applied to all providers (e.g. transport, scope).",
148
+ "properties": {
149
+ "transport": {
150
+ "type": "string",
151
+ "enum": ["querystring", "session"],
152
+ "default": "querystring"
153
+ }
154
+ }
155
+ },
156
+ "providers": {
157
+ "type": "object",
158
+ "description": "Map of OAuth provider names to their configuration. Provider names must match grant's supported provider list (e.g. google, github, facebook). See https://www.npmjs.com/package/grant for all 200+ supported providers.",
159
+ "additionalProperties": {
160
+ "$ref": "#/$defs/oauthProvider"
161
+ }
162
+ }
163
+ }
127
164
  }
128
165
  },
129
166
  "$defs": {
@@ -362,6 +399,31 @@
362
399
  "limit": { "type": "integer", "description": "Maximum number of requests allowed in the time window." },
363
400
  "ttl": { "type": "integer", "description": "Time window in milliseconds." }
364
401
  }
402
+ },
403
+ "oauthProvider": {
404
+ "type": "object",
405
+ "description": "Configuration for a single OAuth provider. The key and secret should be set via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
406
+ "properties": {
407
+ "key": { "type": "string", "description": "OAuth client/app ID. Prefer using OAUTH_<PROVIDER>_KEY env var instead." },
408
+ "secret": { "type": "string", "description": "OAuth client/app secret. Prefer using OAUTH_<PROVIDER>_SECRET env var instead." },
409
+ "scope": {
410
+ "oneOf": [
411
+ { "type": "string" },
412
+ { "type": "array", "items": { "type": "string" } }
413
+ ],
414
+ "description": "OAuth scopes to request (e.g. 'openid email profile')."
415
+ },
416
+ "callback": { "type": "string", "description": "Custom callback URL path. Defaults to /api/auth/oauth/callback." },
417
+ "custom_params": { "type": "object", "description": "Extra query parameters to send to the authorization URL." },
418
+ "subdomain": { "type": "string", "description": "Subdomain for providers that require one (e.g. Shopify)." },
419
+ "nonce": { "type": "boolean", "description": "Enable nonce generation (required by some OIDC providers)." },
420
+ "pkce": { "type": "boolean", "description": "Enable PKCE (Proof Key for Code Exchange) for enhanced security." },
421
+ "response": {
422
+ "type": "array",
423
+ "items": { "type": "string" },
424
+ "description": "Data to include in the callback (e.g. ['tokens', 'profile'])."
425
+ }
426
+ }
365
427
  }
366
428
  }
367
429
  }
@@ -38,17 +38,17 @@ function createBackendSdk(core) {
38
38
  if (!entity) throw new Error(`Single entity not found for slug: ${slug}`);
39
39
  const table = entity.tableName;
40
40
  return {
41
- get() {
42
- const rows = db.findAllSimple(table);
41
+ async get() {
42
+ const rows = await db.findAllSimple(table);
43
43
  return rows[0] || null;
44
44
  },
45
- update(data) {
46
- const rows = db.findAllSimple(table);
45
+ async update(data) {
46
+ const rows = await db.findAllSimple(table);
47
47
  if (!rows[0]) return null;
48
48
  return db.update(table, rows[0].id, data);
49
49
  },
50
- patch(data) {
51
- const rows = db.findAllSimple(table);
50
+ async patch(data) {
51
+ const rows = await db.findAllSimple(table);
52
52
  if (!rows[0]) return null;
53
53
  return db.update(table, rows[0].id, data);
54
54
  },
@@ -82,9 +82,9 @@ function registerApiRoutes(app, core, emit) {
82
82
  };
83
83
 
84
84
  // GET single
85
- router.get(base, mw.read, (_req, res) => {
85
+ router.get(base, mw.read, async (_req, res) => {
86
86
  try {
87
- const rows = db.findAllSimple(table);
87
+ const rows = await db.findAllSimple(table);
88
88
  const row = rows[0];
89
89
  if (!row) return res.status(404).json({ error: 'Not found' });
90
90
  res.json(hide(row));
@@ -94,7 +94,7 @@ function registerApiRoutes(app, core, emit) {
94
94
  // PUT single (full replace)
95
95
  router.put(base, mw.update, async (req, res) => {
96
96
  try {
97
- const rows = db.findAllSimple(table);
97
+ const rows = await db.findAllSimple(table);
98
98
  const row = rows[0];
99
99
  if (!row) return res.status(404).json({ error: 'Not found' });
100
100
  if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
@@ -102,7 +102,7 @@ function registerApiRoutes(app, core, emit) {
102
102
  if (v.errors) return res.status(400).json(v.errors);
103
103
  fireWebhooks(entity, 'beforeUpdate', req.body);
104
104
  const sanitized = sanitizeBody(req.body, entity, true);
105
- const updated = db.update(table, row.id, sanitized);
105
+ const updated = await db.update(table, row.id, sanitized);
106
106
  fireWebhooks(entity, 'afterUpdate', updated);
107
107
  await runMiddlewares('afterUpdate', entity, req, res, sdk);
108
108
  emit(`${entity.name}.updated`, hide(updated));
@@ -113,14 +113,14 @@ function registerApiRoutes(app, core, emit) {
113
113
  // PATCH single (partial)
114
114
  router.patch(base, mw.update, async (req, res) => {
115
115
  try {
116
- const rows = db.findAllSimple(table);
116
+ const rows = await db.findAllSimple(table);
117
117
  const row = rows[0];
118
118
  if (!row) return res.status(404).json({ error: 'Not found' });
119
119
  if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
120
120
  const v = validateBody(req.body, entity, core.groups, { partial: true });
121
121
  if (v.errors) return res.status(400).json(v.errors);
122
122
  fireWebhooks(entity, 'beforeUpdate', req.body);
123
- const updated = db.update(table, row.id, sanitizeBody(req.body, entity));
123
+ const updated = await db.update(table, row.id, sanitizeBody(req.body, entity));
124
124
  fireWebhooks(entity, 'afterUpdate', updated);
125
125
  await runMiddlewares('afterUpdate', entity, req, res, sdk);
126
126
  emit(`${entity.name}.updated`, hide(updated));
@@ -140,37 +140,37 @@ function registerApiRoutes(app, core, emit) {
140
140
  };
141
141
 
142
142
  // GET list (paginated)
143
- router.get(base, mw.read, (req, res) => {
143
+ router.get(base, mw.read, async (req, res) => {
144
144
  try {
145
145
  // Ownership filter: condition: self forces a FK filter on the current user
146
146
  const query = req._selfFilter
147
147
  ? { ...req.query, [req._selfFilter.fk]: req._selfFilter.userId }
148
148
  : req.query;
149
- const result = db.findAll(table, query, {
149
+ const result = await db.findAll(table, query, {
150
150
  page: req.query.page,
151
151
  perPage: req.query.perPage,
152
152
  orderBy: req.query.orderBy,
153
153
  order: req.query.order,
154
154
  });
155
155
  const relations = req.query.relations;
156
- result.data = result.data.map((row) => {
157
- if (relations) db.loadRelations(row, entity, relations);
158
- return hide(row);
159
- });
156
+ for (const row of result.data) {
157
+ if (relations) await db.loadRelations(row, entity, relations);
158
+ }
159
+ result.data = result.data.map((row) => hide(row));
160
160
  res.json(result);
161
161
  } catch (e) { res.status(500).json({ error: e.message }); }
162
162
  });
163
163
 
164
164
  // GET single by id
165
- router.get(`${base}/:id`, mw.read, (req, res) => {
165
+ router.get(`${base}/:id`, mw.read, async (req, res) => {
166
166
  try {
167
- const row = db.findById(table, req.params.id);
167
+ const row = await db.findById(table, req.params.id);
168
168
  if (!row) return res.status(404).json({ error: 'Not found' });
169
169
  // Ownership check for read with condition: self
170
170
  if (req._selfFilter && row[req._selfFilter.fk] !== req._selfFilter.userId) {
171
171
  return res.status(403).json({ error: 'Access denied' });
172
172
  }
173
- if (req.query.relations) db.loadRelations(row, entity, req.query.relations);
173
+ if (req.query.relations) await db.loadRelations(row, entity, req.query.relations);
174
174
  res.json(hide(row));
175
175
  } catch (e) { res.status(500).json({ error: e.message }); }
176
176
  });
@@ -183,8 +183,8 @@ function registerApiRoutes(app, core, emit) {
183
183
  const v = validateBody(body, entity, core.groups);
184
184
  if (v.errors) return res.status(400).json(v.errors);
185
185
  fireWebhooks(entity, 'beforeCreate', body);
186
- const row = db.create(table, sanitizeBody(body, entity));
187
- db.saveBelongsToMany(entity, row.id, req.body);
186
+ const row = await db.create(table, sanitizeBody(body, entity));
187
+ await db.saveBelongsToMany(entity, row.id, req.body);
188
188
  fireWebhooks(entity, 'afterCreate', row);
189
189
  await runMiddlewares('afterCreate', entity, req, res, sdk);
190
190
  emit(`${entity.name}.created`, hide(row));
@@ -195,14 +195,14 @@ function registerApiRoutes(app, core, emit) {
195
195
  // PUT full replace
196
196
  router.put(`${base}/:id`, mw.update, async (req, res) => {
197
197
  try {
198
- if (!db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
198
+ if (!await db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
199
199
  if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
200
200
  const v = validateBody(req.body, entity, core.groups);
201
201
  if (v.errors) return res.status(400).json(v.errors);
202
202
  fireWebhooks(entity, 'beforeUpdate', req.body);
203
203
  const sanitized = sanitizeBody(req.body, entity, true);
204
- const row = db.update(table, req.params.id, sanitized);
205
- db.saveBelongsToMany(entity, row.id, req.body);
204
+ const row = await db.update(table, req.params.id, sanitized);
205
+ await db.saveBelongsToMany(entity, row.id, req.body);
206
206
  fireWebhooks(entity, 'afterUpdate', row);
207
207
  await runMiddlewares('afterUpdate', entity, req, res, sdk);
208
208
  emit(`${entity.name}.updated`, hide(row));
@@ -213,13 +213,13 @@ function registerApiRoutes(app, core, emit) {
213
213
  // PATCH partial update
214
214
  router.patch(`${base}/:id`, mw.update, async (req, res) => {
215
215
  try {
216
- if (!db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
216
+ if (!await db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
217
217
  if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
218
218
  const v = validateBody(req.body, entity, core.groups, { partial: true });
219
219
  if (v.errors) return res.status(400).json(v.errors);
220
220
  fireWebhooks(entity, 'beforeUpdate', req.body);
221
- const row = db.update(table, req.params.id, sanitizeBody(req.body, entity));
222
- db.saveBelongsToMany(entity, row.id, req.body);
221
+ const row = await db.update(table, req.params.id, sanitizeBody(req.body, entity));
222
+ await db.saveBelongsToMany(entity, row.id, req.body);
223
223
  fireWebhooks(entity, 'afterUpdate', row);
224
224
  await runMiddlewares('afterUpdate', entity, req, res, sdk);
225
225
  emit(`${entity.name}.updated`, hide(row));
@@ -230,11 +230,11 @@ function registerApiRoutes(app, core, emit) {
230
230
  // DELETE
231
231
  router.delete(`${base}/:id`, mw.delete, async (req, res) => {
232
232
  try {
233
- const existing = db.findById(table, req.params.id);
233
+ const existing = await db.findById(table, req.params.id);
234
234
  if (!existing) return res.status(404).json({ error: 'Not found' });
235
235
  if (!await runMiddlewares('beforeDelete', entity, req, res, sdk)) return;
236
236
  fireWebhooks(entity, 'beforeDelete', existing);
237
- const row = db.remove(table, req.params.id);
237
+ const row = await db.remove(table, req.params.id);
238
238
  fireWebhooks(entity, 'afterDelete', row);
239
239
  await runMiddlewares('afterDelete', entity, req, res, sdk);
240
240
  emit(`${entity.name}.deleted`, hide(row));
@@ -267,8 +267,8 @@ function policyMiddleware(rule, entity, core) {
267
267
  break;
268
268
  }
269
269
  const allowed = Array.isArray(p.allow) ? p.allow : [p.allow];
270
- middlewares = [(req, res, next) => {
271
- const { user, apiKeyPermissions, error } = resolveAuthHeader(req.headers.authorization);
270
+ middlewares = [async (req, res, next) => {
271
+ const { user, apiKeyPermissions, error } = await resolveAuthHeader(req.headers.authorization);
272
272
  if (!user) return res.status(401).json({ error: 'Authorization required' });
273
273
  if (error === 'invalid_token') return res.status(401).json({ error: 'Invalid or expired token' });
274
274
  if (!allowed.includes(user.entity)) return res.status(403).json({ error: 'Access denied' });
@@ -277,7 +277,7 @@ function policyMiddleware(rule, entity, core) {
277
277
  try {
278
278
  // Ownership-based access: condition: self
279
279
  if (p.condition === 'self') {
280
- enforceSelfCondition(rule, entity, req, core);
280
+ await enforceSelfCondition(rule, entity, req, core);
281
281
  }
282
282
  next();
283
283
  } catch (e) {
@@ -328,7 +328,7 @@ function _apiKeyPermGuard(operation, entity) {
328
328
  * - update: ensure the record belongs to the user, disallow ownership change
329
329
  * - delete: ensure the record belongs to the user
330
330
  */
331
- function enforceSelfCondition(rule, entity, req, core) {
331
+ async function enforceSelfCondition(rule, entity, req, core) {
332
332
  const userId = req.user.id;
333
333
  const userEntity = req.user.entity;
334
334
 
@@ -351,7 +351,7 @@ function enforceSelfCondition(rule, entity, req, core) {
351
351
  } else if (rule === 'update' || rule === 'delete') {
352
352
  // Verify the record belongs to the user
353
353
  if (req.params && req.params.id) {
354
- const row = db.findById(entity.tableName, req.params.id);
354
+ const row = await db.findById(entity.tableName, req.params.id);
355
355
  if (row && row[fk] !== userId) {
356
356
  const err = new Error('Access denied: record does not belong to you');
357
357
  err.status = 403;