opacacms 0.3.13 → 0.3.15
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/LICENSE +21 -0
- package/README.md +196 -173
- package/dist/{chunk-8hhzvesq.js → chunk-5b9eqr34.js} +3 -2
- package/dist/{chunk-1vtdkx5e.js → chunk-dz5bh1bd.js} +7 -2
- package/dist/{chunk-gj8w1r1e.js → chunk-nv91gc63.js} +18 -7
- package/dist/{chunk-xdmw9vzq.js → chunk-nz6xhtja.js} +7 -2
- package/dist/{chunk-0njhbe4a.js → chunk-qsefknd3.js} +7 -2
- package/dist/{chunk-2ec9fsgr.js → chunk-tsmhn78f.js} +6 -2
- package/dist/client.d.ts +5 -0
- package/dist/client.js +1 -1
- package/dist/config.d.ts +8 -1
- package/dist/db/better-sqlite.js +1 -1
- package/dist/db/bun-sqlite.js +1 -1
- package/dist/db/d1.js +1 -1
- package/dist/db/index.js +5 -5
- package/dist/db/postgres.js +1 -1
- package/dist/db/sqlite.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/schema/collection.d.ts +9 -8
- package/dist/schema/global.d.ts +8 -13
- package/dist/types/access.d.ts +30 -0
- package/dist/types.d.ts +5 -27
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Francy Santos (fhorray)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -123,31 +123,32 @@ A **Collection** is a database table + a REST API. Pure magic. ✨
|
|
|
123
123
|
|
|
124
124
|
```typescript
|
|
125
125
|
// collections/posts.ts
|
|
126
|
-
import {
|
|
127
|
-
|
|
128
|
-
export const posts =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
126
|
+
import { defineCollection, z } from 'opacacms';
|
|
127
|
+
|
|
128
|
+
export const posts = defineCollection({
|
|
129
|
+
slug: 'posts',
|
|
130
|
+
label: 'Blog Posts',
|
|
131
|
+
admin: {
|
|
132
|
+
icon: 'FileText',
|
|
133
|
+
useAsTitle: 'title',
|
|
134
|
+
},
|
|
135
|
+
schema: z.object({
|
|
136
|
+
title: z.text({ label: 'Post Title' }).required(),
|
|
137
|
+
slug: z.slug({ from: 'title' }).unique(),
|
|
138
|
+
content: z.richText({ label: 'Content' }).localized(),
|
|
139
|
+
author: z.relationship('_users', { label: 'Author' }),
|
|
140
|
+
status: z
|
|
141
|
+
.select(['draft', 'published'], { label: 'Status' })
|
|
142
|
+
.default('draft'),
|
|
143
|
+
featured: z.boolean({ label: 'Featured Post' }),
|
|
144
|
+
}),
|
|
145
|
+
access: {
|
|
145
146
|
read: () => true,
|
|
146
147
|
create: ({ user }) => !!user,
|
|
147
148
|
update: ({ user }) => user?.role === 'admin',
|
|
148
149
|
delete: ({ user }) => user?.role === 'admin',
|
|
149
|
-
}
|
|
150
|
-
|
|
150
|
+
},
|
|
151
|
+
hooks: {
|
|
151
152
|
beforeCreate: async (data) => {
|
|
152
153
|
// Mutate data before insertion
|
|
153
154
|
return { ...data, publishedAt: new Date().toISOString() };
|
|
@@ -155,104 +156,90 @@ export const posts = Collection.create('posts')
|
|
|
155
156
|
afterCreate: async (doc) => {
|
|
156
157
|
console.log('New post created:', doc.id);
|
|
157
158
|
},
|
|
158
|
-
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
159
161
|
```
|
|
160
162
|
|
|
161
|
-
### Collection
|
|
163
|
+
### Collection Configuration
|
|
162
164
|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
|
165
|
+
| Property | Description |
|
|
166
|
+
| ------------ | --------------------------------------------------------- |
|
|
167
|
+
| `slug` | The unique identifier for the collection |
|
|
168
|
+
| `label` | Display name used in the Admin UI |
|
|
169
|
+
| `admin` | UI configuration (`icon`, `useAsTitle`, `defaultColumns`) |
|
|
170
|
+
| `schema` | A `z.object` defining the data structure |
|
|
171
|
+
| `access` | Collection-level access control |
|
|
172
|
+
| `hooks` | Lifecycle hooks (`beforeCreate`, `afterUpdate`, etc.) |
|
|
173
|
+
| `webhooks` | External webhook notifications |
|
|
174
|
+
| `versions` | Enable document versioning with history |
|
|
175
|
+
| `timestamps` | Customize or disable timestamp fields |
|
|
174
176
|
|
|
175
177
|
---
|
|
176
178
|
|
|
177
179
|
## 🧪 Field Types
|
|
178
180
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
|
182
|
-
|
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
181
|
+
| Field | Usage | Description |
|
|
182
|
+
| ------------------ | ------------------------------------------------------- | --------------------------------------------- |
|
|
183
|
+
| `z.text()` | `z.text({ label: 'Title' })` | Simple string input |
|
|
184
|
+
| `z.textarea()` | `z.textarea({ label: 'Bio' })` | Multi-line text area |
|
|
185
|
+
| `z.number()` | `z.number({ label: 'Price' })` | Numeric input |
|
|
186
|
+
| `z.richText()` | `z.richText({ label: 'Content' })` | Block-based Lexical editor (Notion style!) 📝 |
|
|
187
|
+
| `z.relationship()` | `z.relationship('_users', { label: 'Author' })` | Links to another collection |
|
|
188
|
+
| `z.file()` | `z.file({ label: 'Image' })` | File/image upload ☁️ |
|
|
189
|
+
| `z.blocks()` | `z.blocks([hero, gallery], { label: 'Layout' })` | Dynamic page builder 🧱 |
|
|
190
|
+
| `z.group()` | `z.group({ meta1: z.text(), meta2: z.text() })` | Nested object group |
|
|
191
|
+
| `z.array()` | `z.array(z.object({ tag: z.text() }))` | Repeatable field array |
|
|
192
|
+
| `z.select()` | `z.select(['draft', 'published'], { label: 'Status' })` | Dropdown picker |
|
|
193
|
+
| `z.boolean()` | `z.boolean({ label: 'Active' })` | Boolean toggle |
|
|
194
|
+
| `z.slug()` | `z.slug({ from: 'title', label: 'Slug' })` | Auto-generated URL slug |
|
|
195
|
+
| `z.date()` | `z.date({ label: 'Published At' })` | Date/time picker |
|
|
196
|
+
| `z.tabs()` | `z.tabs({ SEO: z.object({...}) })` | UI-only grouping for the admin |
|
|
197
|
+
| `z.join()` | `z.join('posts', { foreignKey: 'author' })` | Virtual reversed relationship (read-only) |
|
|
198
|
+
| `z.password()` | `z.password({ label: 'Password' })` | Secure, hashed input |
|
|
197
199
|
|
|
198
200
|
### Common Field Methods
|
|
199
201
|
|
|
200
202
|
Every field type inherits these chainable methods from the base builder:
|
|
201
203
|
|
|
202
204
|
```typescript
|
|
203
|
-
|
|
204
|
-
.
|
|
205
|
-
.placeholder('you@example.com') // Input placeholder
|
|
206
|
-
.required() // Mark as required
|
|
205
|
+
z.text({ label: 'Email Address', description: 'Primary email' })
|
|
206
|
+
.required() // Native Zod or custom required condition
|
|
207
207
|
.unique() // Unique constraint in the DB
|
|
208
208
|
.localized() // Enable per-locale values (i18n)
|
|
209
|
-
.
|
|
210
|
-
.
|
|
211
|
-
.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
.default('hello@world.com') // Native Zod default value
|
|
210
|
+
.email('Invalid email address') // Native Zod validation
|
|
211
|
+
.cms({
|
|
212
|
+
admin: {
|
|
213
|
+
readOnly: true,
|
|
214
|
+
hidden: false,
|
|
215
|
+
components: { Field: 'my-custom-field' },
|
|
216
|
+
},
|
|
217
|
+
}); // Advanced CMS and Admin UI options
|
|
216
218
|
```
|
|
217
219
|
|
|
218
220
|
---
|
|
219
221
|
|
|
220
222
|
## ✅ Validation
|
|
221
223
|
|
|
222
|
-
OpacaCMS supports
|
|
223
|
-
|
|
224
|
-
### Custom Function Validation
|
|
225
|
-
|
|
226
|
-
Return `true` to pass, or a `string` error message to fail:
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
Field.text('username').validate((value) => {
|
|
230
|
-
if (value === 'admin') return "Username 'admin' is reserved";
|
|
231
|
-
return true;
|
|
232
|
-
});
|
|
233
|
-
```
|
|
224
|
+
OpacaCMS natively supports validation since the entire schema is built using Zod. You can chain standard Zod validation methods directly to your fields.
|
|
234
225
|
|
|
235
226
|
### Zod Schema Validation
|
|
236
227
|
|
|
237
|
-
|
|
228
|
+
Errors are automatically mapped and handled by the API and Admin UI:
|
|
238
229
|
|
|
239
230
|
```typescript
|
|
240
231
|
import { z } from 'opacacms';
|
|
241
232
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
233
|
+
// Regular expressions
|
|
234
|
+
z.text().regex(/^\d{3}\.\d{3}\.\d{3}-\d{2}$/, 'Invalid CPF format');
|
|
245
235
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.regex(/[A-Z]/, 'Must contain at least one uppercase letter'),
|
|
251
|
-
);
|
|
236
|
+
// String constraints
|
|
237
|
+
z.text()
|
|
238
|
+
.min(8, 'Password must be at least 8 characters')
|
|
239
|
+
.regex(/[A-Z]/, 'Must contain at least one uppercase letter');
|
|
252
240
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.validate(z.string().email('Invalid email address'));
|
|
241
|
+
// Built-in format validators
|
|
242
|
+
z.text().required().email('Invalid email address');
|
|
256
243
|
```
|
|
257
244
|
|
|
258
245
|
> **💡 Tip:** Zod validation integrates seamlessly with `.required()`. The built-in requirement check runs first, then your Zod schema validates the value shape.
|
|
@@ -264,17 +251,24 @@ Field.text('email')
|
|
|
264
251
|
Globals are **singleton documents** — perfect for site settings, navigation, footers, and other one-of-a-kind configs.
|
|
265
252
|
|
|
266
253
|
```typescript
|
|
267
|
-
import {
|
|
254
|
+
import { defineGlobal, z } from 'opacacms';
|
|
268
255
|
|
|
269
|
-
export const siteSettings =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
256
|
+
export const siteSettings = defineGlobal({
|
|
257
|
+
slug: 'site-settings',
|
|
258
|
+
label: 'Site Settings',
|
|
259
|
+
admin: {
|
|
260
|
+
icon: 'Settings',
|
|
261
|
+
},
|
|
262
|
+
schema: z.object({
|
|
263
|
+
siteName: z.text().required().default('My Shiny App'),
|
|
264
|
+
tagline: z.text().localized(),
|
|
265
|
+
logo: z.file(),
|
|
266
|
+
social: z.group({
|
|
267
|
+
twitter: z.text(),
|
|
268
|
+
github: z.text(),
|
|
269
|
+
}),
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
278
272
|
```
|
|
279
273
|
|
|
280
274
|
API: `GET /api/globals/site-settings` and `PUT /api/globals/site-settings`
|
|
@@ -288,25 +282,31 @@ Secure your data with simple functions at both **collection** and **field** leve
|
|
|
288
282
|
### Collection-Level Access
|
|
289
283
|
|
|
290
284
|
```typescript
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
})
|
|
285
|
+
defineCollection({
|
|
286
|
+
// ...
|
|
287
|
+
access: {
|
|
288
|
+
read: ({ user }) => !!user, // Logged in? You're good.
|
|
289
|
+
create: ({ user }) => user?.role === 'admin', // Only admins please!
|
|
290
|
+
update: ({ data, user }) => data.ownerId === user.id, // Only your own stuff.
|
|
291
|
+
delete: ({ user }) => user?.role === 'admin',
|
|
292
|
+
requireApiKey: true, // Require API key for programmatic access
|
|
293
|
+
},
|
|
294
|
+
});
|
|
298
295
|
```
|
|
299
296
|
|
|
300
297
|
### Field-Level Access
|
|
301
298
|
|
|
302
|
-
Control visibility and editability per-field:
|
|
299
|
+
Control visibility and editability per-field using Zod conditional methods and CMS metadata:
|
|
303
300
|
|
|
304
301
|
```typescript
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
302
|
+
z.text({ label: 'Internal Notes' })
|
|
303
|
+
.condition((data) => data.role === 'admin') // Only show if another field matches
|
|
304
|
+
.cms({
|
|
305
|
+
admin: {
|
|
306
|
+
hidden: true, // Hide from Admin UI
|
|
307
|
+
readOnly: false,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
310
|
```
|
|
311
311
|
|
|
312
312
|
### Role-Based Access Control (RBAC)
|
|
@@ -334,30 +334,33 @@ access: {
|
|
|
334
334
|
Hooks let you run side-effects at key points in the document lifecycle. They receive the document data and can mutate it before persistence.
|
|
335
335
|
|
|
336
336
|
```typescript
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
337
|
+
defineCollection({
|
|
338
|
+
// ...
|
|
339
|
+
hooks: {
|
|
340
|
+
beforeCreate: async (data) => {
|
|
341
|
+
// Transform or enrich data before saving
|
|
342
|
+
data.slug = data.title.toLowerCase().replace(/\s+/g, '-');
|
|
343
|
+
return data;
|
|
344
|
+
},
|
|
345
|
+
afterCreate: async (doc) => {
|
|
346
|
+
// Side-effects after the document is saved
|
|
347
|
+
await sendWelcomeEmail(doc.email);
|
|
348
|
+
},
|
|
349
|
+
beforeUpdate: async (data) => {
|
|
350
|
+
data.updatedBy = 'system';
|
|
351
|
+
return data;
|
|
352
|
+
},
|
|
353
|
+
afterUpdate: async (doc) => {
|
|
354
|
+
await invalidateCache(`/posts/${doc.slug}`);
|
|
355
|
+
},
|
|
356
|
+
beforeDelete: async (id) => {
|
|
357
|
+
await archiveDocument(id);
|
|
358
|
+
},
|
|
359
|
+
afterDelete: async (id) => {
|
|
360
|
+
console.log(`Document ${id} deleted`);
|
|
361
|
+
},
|
|
359
362
|
},
|
|
360
|
-
})
|
|
363
|
+
});
|
|
361
364
|
```
|
|
362
365
|
|
|
363
366
|
---
|
|
@@ -367,24 +370,25 @@ Hooks let you run side-effects at key points in the document lifecycle. They rec
|
|
|
367
370
|
Send HTTP notifications to external services when documents change. Webhooks run in the background using `waitUntil` on supported runtimes (Cloudflare Workers, Vercel Edge).
|
|
368
371
|
|
|
369
372
|
```typescript
|
|
370
|
-
import { defineCollection } from 'opacacms';
|
|
371
|
-
|
|
372
373
|
defineCollection({
|
|
373
374
|
slug: 'orders',
|
|
374
375
|
webhooks: [
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
]
|
|
376
|
+
{
|
|
377
|
+
type: 'outgoing',
|
|
378
|
+
url: 'https://hooks.slack.com/services/xxx',
|
|
379
|
+
events: ['after:create', 'after:update'],
|
|
380
|
+
headers: { Authorization: 'Bearer my-token' },
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
type: 'outgoing',
|
|
384
|
+
url: 'https://api.example.com/webhooks/orders',
|
|
385
|
+
events: ['after:delete'],
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
});
|
|
385
389
|
```
|
|
386
390
|
|
|
387
|
-
Supported events: `
|
|
391
|
+
Supported events: `after:create`, `after:update`, `after:delete`. OpacaCMS also supports `incoming` webhooks.
|
|
388
392
|
|
|
389
393
|
---
|
|
390
394
|
|
|
@@ -393,9 +397,11 @@ Supported events: `afterCreate`, `afterUpdate`, `afterDelete`.
|
|
|
393
397
|
Enable document versioning to keep a full history of changes. Each save creates a snapshot that can be restored later.
|
|
394
398
|
|
|
395
399
|
```typescript
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
defineCollection({
|
|
401
|
+
slug: 'posts',
|
|
402
|
+
versions: { drafts: true, maxRevisions: 10 },
|
|
403
|
+
schema: z.object({...})
|
|
404
|
+
});
|
|
399
405
|
```
|
|
400
406
|
|
|
401
407
|
### Version API
|
|
@@ -411,13 +417,27 @@ The admin UI provides a visual "Versions" panel where editors can browse and res
|
|
|
411
417
|
|
|
412
418
|
## 🧮 Virtual Fields
|
|
413
419
|
|
|
414
|
-
Virtual fields are **computed at read-time** and never stored in the database. Perfect for derived values, aggregated data, or API mashups.
|
|
420
|
+
Virtual fields are **computed at read-time** and never stored in the database. Perfect for derived values, aggregated data, or API mashups. Use `z.helpers()` to infer the collection types for type-safe resolving.
|
|
415
421
|
|
|
416
422
|
```typescript
|
|
417
|
-
|
|
423
|
+
const userShape = z.object({
|
|
424
|
+
firstName: z.text(),
|
|
425
|
+
lastName: z.text(),
|
|
426
|
+
});
|
|
418
427
|
|
|
419
|
-
|
|
420
|
-
|
|
428
|
+
const { meta } = z.helpers(userShape);
|
|
429
|
+
|
|
430
|
+
export const users = defineCollection({
|
|
431
|
+
slug: 'users',
|
|
432
|
+
schema: z.object({
|
|
433
|
+
...userShape.shape,
|
|
434
|
+
fullName: meta(z.text(), {
|
|
435
|
+
isVirtual: true,
|
|
436
|
+
resolve: async ({ data, user, req }) => {
|
|
437
|
+
return `${data.firstName} ${data.lastName}`;
|
|
438
|
+
},
|
|
439
|
+
}),
|
|
440
|
+
}),
|
|
421
441
|
});
|
|
422
442
|
```
|
|
423
443
|
|
|
@@ -494,12 +514,13 @@ logger.info('Custom route hit');
|
|
|
494
514
|
|
|
495
515
|
## 🗄 Database Adapters
|
|
496
516
|
|
|
497
|
-
OpacaCMS provides first-class adapters for multiple database engines.
|
|
517
|
+
OpacaCMS provides first-class adapters for multiple database engines.
|
|
498
518
|
|
|
499
519
|
| Adapter | Import | Usage |
|
|
500
520
|
| ------------- | ------------- | --------------------------------- |
|
|
501
521
|
| SQLite (Bun) | `opacacms/db` | `createSQLiteAdapter('local.db')` |
|
|
502
522
|
| Cloudflare D1 | `opacacms/db` | `createD1Adapter(env.DB)` |
|
|
523
|
+
| PostgreSQL | `opacacms/db` | `createPostgresAdapter(url)` |
|
|
503
524
|
|
|
504
525
|
---
|
|
505
526
|
|
|
@@ -552,8 +573,8 @@ i18n: {
|
|
|
552
573
|
}
|
|
553
574
|
|
|
554
575
|
// Field
|
|
555
|
-
|
|
556
|
-
|
|
576
|
+
z.text({ label: 'Title' }).localized()
|
|
577
|
+
z.richText({ label: 'Content' }).localized()
|
|
557
578
|
```
|
|
558
579
|
|
|
559
580
|
### Locale Selection
|
|
@@ -583,7 +604,7 @@ This is where OpacaCMS shines. You can replace any field UI with your own **Reac
|
|
|
583
604
|
|
|
584
605
|
```tsx
|
|
585
606
|
// MyColorPicker.tsx
|
|
586
|
-
import { defineReactField } from 'opacacms';
|
|
607
|
+
import { defineReactField } from 'opacacms/admin';
|
|
587
608
|
|
|
588
609
|
const ColorPicker = ({ value, onChange }) => (
|
|
589
610
|
<input
|
|
@@ -600,7 +621,7 @@ defineReactField('my-color-picker', ColorPicker);
|
|
|
600
621
|
|
|
601
622
|
```tsx
|
|
602
623
|
// MyVuePicker.vue
|
|
603
|
-
import { defineVueField } from 'opacacms';
|
|
624
|
+
import { defineVueField } from 'opacacms/admin';
|
|
604
625
|
import { createApp } from 'vue';
|
|
605
626
|
import MyVueComponent from './MyVueComponent.vue';
|
|
606
627
|
|
|
@@ -610,9 +631,11 @@ defineVueField('my-vue-picker', MyVueComponent, { createApp });
|
|
|
610
631
|
### 3️⃣ Reference in Schema
|
|
611
632
|
|
|
612
633
|
```typescript
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
634
|
+
z.text({ label: 'Color' }).cms({
|
|
635
|
+
admin: {
|
|
636
|
+
components: {
|
|
637
|
+
Field: 'my-color-picker',
|
|
638
|
+
},
|
|
616
639
|
},
|
|
617
640
|
});
|
|
618
641
|
```
|
|
@@ -635,11 +658,13 @@ Collections and Fields can be further customized for the Admin UI using the `.ad
|
|
|
635
658
|
Example:
|
|
636
659
|
|
|
637
660
|
```typescript
|
|
638
|
-
export const InternalData =
|
|
639
|
-
|
|
661
|
+
export const InternalData = defineCollection({
|
|
662
|
+
slug: 'internal_data',
|
|
663
|
+
admin: {
|
|
640
664
|
hidden: true, // Only accessible via direct link
|
|
641
|
-
}
|
|
642
|
-
.
|
|
665
|
+
},
|
|
666
|
+
schema: z.object({...}),
|
|
667
|
+
});
|
|
643
668
|
```
|
|
644
669
|
|
|
645
670
|
---
|
|
@@ -917,8 +942,8 @@ export const myPlugin = () =>
|
|
|
917
942
|
icon: "Activity",
|
|
918
943
|
path: "/admin/my-plugin",
|
|
919
944
|
render: (serverUrl) => \`
|
|
920
|
-
<iframe
|
|
921
|
-
src="\${serverUrl}/api/plugins/my-plugin/view"
|
|
945
|
+
<iframe
|
|
946
|
+
src="\${serverUrl}/api/plugins/my-plugin/view"
|
|
922
947
|
style="width:100%; height:calc(100vh - 100px); border:none;"
|
|
923
948
|
></iframe>
|
|
924
949
|
\`
|
|
@@ -929,14 +954,12 @@ export const myPlugin = () =>
|
|
|
929
954
|
|
|
930
955
|
// Serve the Isolated HTML View with Hono/HTML
|
|
931
956
|
app.get('/api/plugins/my-plugin/view', (c) => {
|
|
932
|
-
return c.html(
|
|
933
|
-
<body
|
|
934
|
-
style="background: #f6f9fc; padding: 40px; font-family: sans-serif;"
|
|
935
|
-
>
|
|
957
|
+
return (c.header('Content-Type', 'text/html'), c.body(\`
|
|
958
|
+
<body style="background: #f6f9fc; padding: 40px; font-family: sans-serif;">
|
|
936
959
|
<h1>Modern Plugin UI</h1>
|
|
937
960
|
<p>Isolated from CMS styles with zero boilerplate.</p>
|
|
938
961
|
</body>
|
|
939
|
-
|
|
962
|
+
\`));
|
|
940
963
|
});
|
|
941
964
|
},
|
|
942
965
|
|
|
@@ -335,6 +335,7 @@ class D1Adapter extends BaseDatabaseAdapter {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
const totalPages = Math.ceil(total / limit);
|
|
338
|
+
const hasNextPage = page * limit < total;
|
|
338
339
|
return {
|
|
339
340
|
docs: docs.filter(Boolean),
|
|
340
341
|
totalDocs: total,
|
|
@@ -342,11 +343,11 @@ class D1Adapter extends BaseDatabaseAdapter {
|
|
|
342
343
|
totalPages,
|
|
343
344
|
page,
|
|
344
345
|
pagingCounter: offset + 1,
|
|
345
|
-
hasNextPage
|
|
346
|
+
hasNextPage,
|
|
346
347
|
hasPrevPage: page > 1 || !!options?.after || !!options?.before,
|
|
347
348
|
prevPage: page > 1 ? page - 1 : null,
|
|
348
349
|
nextPage: page < totalPages ? page + 1 : null,
|
|
349
|
-
nextCursor: docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
350
|
+
nextCursor: docs.length > 0 && hasNextPage ? docs[docs.length - 1][cursorColumn] : null,
|
|
350
351
|
prevCursor: docs.length > 0 && (page > 1 || !!options?.after || !!options?.before) ? docs[0][cursorColumn] : null
|
|
351
352
|
};
|
|
352
353
|
}
|
|
@@ -126,6 +126,10 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
126
126
|
filteredData[col] = flatData[col];
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
if (!filteredData.id) {
|
|
130
|
+
filteredData.id = crypto.randomUUID();
|
|
131
|
+
flatData.id = filteredData.id;
|
|
132
|
+
}
|
|
129
133
|
await tx.insertInto(tableName).values(filteredData).execute();
|
|
130
134
|
for (const [key, values] of Object.entries(hasManyData)) {
|
|
131
135
|
const joinTableName = `${tableName}_${toSnakeCase(key)}_relations`.toLowerCase();
|
|
@@ -327,6 +331,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
327
331
|
}
|
|
328
332
|
}
|
|
329
333
|
const totalPages = Math.ceil(total / limit);
|
|
334
|
+
const hasNextPage = page * limit < total;
|
|
330
335
|
return {
|
|
331
336
|
docs: docs.filter(Boolean),
|
|
332
337
|
totalDocs: total,
|
|
@@ -334,11 +339,11 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
334
339
|
totalPages,
|
|
335
340
|
page,
|
|
336
341
|
pagingCounter: offset + 1,
|
|
337
|
-
hasNextPage
|
|
342
|
+
hasNextPage,
|
|
338
343
|
hasPrevPage: page > 1 || !!options?.after || !!options?.before,
|
|
339
344
|
prevPage: page > 1 ? page - 1 : null,
|
|
340
345
|
nextPage: page < totalPages ? page + 1 : null,
|
|
341
|
-
nextCursor: docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
346
|
+
nextCursor: hasNextPage && docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
342
347
|
prevCursor: docs.length > 0 && (page > 1 || !!options?.after || !!options?.before) ? docs[0][cursorColumn] : null
|
|
343
348
|
};
|
|
344
349
|
}
|
|
@@ -16,9 +16,14 @@ class OpacaError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
function createClient(configOrOptions) {
|
|
19
|
-
const
|
|
19
|
+
const isConfig = configOrOptions.serverURL || configOrOptions.collections;
|
|
20
|
+
const options = isConfig ? {
|
|
21
|
+
baseURL: configOrOptions.serverURL || "http://localhost:3000",
|
|
22
|
+
token: configOrOptions.token,
|
|
23
|
+
api: configOrOptions.api
|
|
24
|
+
} : configOrOptions;
|
|
20
25
|
const baseURL = options.baseURL.replace(/\/$/, "");
|
|
21
|
-
let engineConfig = null;
|
|
26
|
+
let engineConfig = options.api || null;
|
|
22
27
|
let metadataCache = null;
|
|
23
28
|
const getHeaders = () => {
|
|
24
29
|
const headers = {
|
|
@@ -45,18 +50,20 @@ function createClient(configOrOptions) {
|
|
|
45
50
|
return res.json();
|
|
46
51
|
};
|
|
47
52
|
const initConfig = async () => {
|
|
48
|
-
if (engineConfig && metadataCache)
|
|
53
|
+
if (engineConfig && engineConfig.graphql && metadataCache)
|
|
49
54
|
return;
|
|
50
55
|
try {
|
|
51
56
|
const [setupRes, metadataRes] = await Promise.all([
|
|
52
57
|
fetcher("/api/__admin/setup"),
|
|
53
58
|
fetcher("/api/__admin/collections")
|
|
54
59
|
]);
|
|
55
|
-
engineConfig = setupRes.api || { rest: true, graphql: { enabled: false, path: "/graphql" } };
|
|
60
|
+
engineConfig = setupRes.api || engineConfig || { rest: true, graphql: { enabled: false, path: "/graphql" } };
|
|
56
61
|
metadataCache = metadataRes;
|
|
57
62
|
} catch (e) {
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
const localCols = configOrOptions.collections || [];
|
|
64
|
+
const localGlobals = configOrOptions.globals || [];
|
|
65
|
+
engineConfig = engineConfig || { rest: true, graphql: { enabled: false, path: "/graphql" } };
|
|
66
|
+
metadataCache = { collections: localCols, globals: localGlobals };
|
|
60
67
|
}
|
|
61
68
|
};
|
|
62
69
|
const buildSelectionSet = (fields) => {
|
|
@@ -86,7 +93,11 @@ function createClient(configOrOptions) {
|
|
|
86
93
|
if (!engineConfig?.graphql?.enabled) {
|
|
87
94
|
throw new OpacaError("GraphQL is not enabled on this server.", 400);
|
|
88
95
|
}
|
|
89
|
-
|
|
96
|
+
let gqlPath = engineConfig.graphql.path || "/graphql";
|
|
97
|
+
if (!gqlPath.startsWith("/api") && !gqlPath.startsWith("http")) {
|
|
98
|
+
gqlPath = `/api${gqlPath.startsWith("/") ? "" : "/"}${gqlPath}`;
|
|
99
|
+
}
|
|
100
|
+
return fetcher(gqlPath, {
|
|
90
101
|
method: "POST",
|
|
91
102
|
body: JSON.stringify({ query, variables })
|
|
92
103
|
});
|
|
@@ -125,6 +125,10 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
125
125
|
filteredData[col] = flatData[col];
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
if (!filteredData.id) {
|
|
129
|
+
filteredData.id = crypto.randomUUID();
|
|
130
|
+
flatData.id = filteredData.id;
|
|
131
|
+
}
|
|
128
132
|
await tx.insertInto(tableName).values(filteredData).execute();
|
|
129
133
|
for (const [key, values] of Object.entries(hasManyData)) {
|
|
130
134
|
const joinTableName = `${tableName}_${toSnakeCase(key)}_relations`.toLowerCase();
|
|
@@ -340,6 +344,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
340
344
|
}
|
|
341
345
|
}
|
|
342
346
|
const totalPages = Math.ceil(total / limit);
|
|
347
|
+
const hasNextPage = page * limit < total;
|
|
343
348
|
return {
|
|
344
349
|
docs: docs.filter(Boolean),
|
|
345
350
|
totalDocs: total,
|
|
@@ -347,11 +352,11 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
347
352
|
totalPages,
|
|
348
353
|
page,
|
|
349
354
|
pagingCounter: offset + 1,
|
|
350
|
-
hasNextPage
|
|
355
|
+
hasNextPage,
|
|
351
356
|
hasPrevPage: page > 1 || !!options?.after || !!options?.before,
|
|
352
357
|
prevPage: page > 1 ? page - 1 : null,
|
|
353
358
|
nextPage: page < totalPages ? page + 1 : null,
|
|
354
|
-
nextCursor: docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
359
|
+
nextCursor: hasNextPage && docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
355
360
|
prevCursor: docs.length > 0 && (page > 1 || !!options?.after || !!options?.before) ? docs[0][cursorColumn] : null
|
|
356
361
|
};
|
|
357
362
|
}
|
|
@@ -117,6 +117,10 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
117
117
|
filteredData[col] = flatData[col];
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
if (!filteredData.id) {
|
|
121
|
+
filteredData.id = crypto.randomUUID();
|
|
122
|
+
flatData.id = filteredData.id;
|
|
123
|
+
}
|
|
120
124
|
await this.db.insertInto(tableName).values(filteredData).execute();
|
|
121
125
|
for (const [key, values] of Object.entries(hasManyData)) {
|
|
122
126
|
const joinTableName = `${tableName}_${toSnakeCase(key)}_relations`.toLowerCase();
|
|
@@ -331,6 +335,7 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
331
335
|
}
|
|
332
336
|
}
|
|
333
337
|
const totalPages = Math.ceil(total / limit);
|
|
338
|
+
const hasNextPage = page * limit < total;
|
|
334
339
|
return {
|
|
335
340
|
docs: docs.filter(Boolean),
|
|
336
341
|
totalDocs: total,
|
|
@@ -338,11 +343,11 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
338
343
|
totalPages,
|
|
339
344
|
page,
|
|
340
345
|
pagingCounter: offset + 1,
|
|
341
|
-
hasNextPage
|
|
346
|
+
hasNextPage,
|
|
342
347
|
hasPrevPage: page > 1 || !!options?.after || !!options?.before,
|
|
343
348
|
prevPage: page > 1 ? page - 1 : null,
|
|
344
349
|
nextPage: page < totalPages ? page + 1 : null,
|
|
345
|
-
nextCursor: docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
350
|
+
nextCursor: hasNextPage && docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
346
351
|
prevCursor: docs.length > 0 && (page > 1 || !!options?.after || !!options?.before) ? docs[0][cursorColumn] : null
|
|
347
352
|
};
|
|
348
353
|
}
|
|
@@ -150,6 +150,9 @@ class PostgresAdapter extends BaseDatabaseAdapter {
|
|
|
150
150
|
delete flatData[key];
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
if (!flatData.id) {
|
|
154
|
+
flatData.id = crypto.randomUUID();
|
|
155
|
+
}
|
|
153
156
|
await tx.insertInto(tableName).values(flatData).execute();
|
|
154
157
|
for (const [key, values] of Object.entries(hasManyData)) {
|
|
155
158
|
const joinTableName = `${tableName}_${toSnakeCase(key)}_relations`.toLowerCase();
|
|
@@ -360,6 +363,7 @@ class PostgresAdapter extends BaseDatabaseAdapter {
|
|
|
360
363
|
}
|
|
361
364
|
}
|
|
362
365
|
const totalPages = Math.ceil(total / limit);
|
|
366
|
+
const hasNextPage = page * limit < total;
|
|
363
367
|
return {
|
|
364
368
|
docs: docs.filter(Boolean),
|
|
365
369
|
totalDocs: total,
|
|
@@ -367,11 +371,11 @@ class PostgresAdapter extends BaseDatabaseAdapter {
|
|
|
367
371
|
totalPages,
|
|
368
372
|
page,
|
|
369
373
|
pagingCounter: offset + 1,
|
|
370
|
-
hasNextPage
|
|
374
|
+
hasNextPage,
|
|
371
375
|
hasPrevPage: page > 1 || !!options?.after || !!options?.before,
|
|
372
376
|
prevPage: page > 1 ? page - 1 : null,
|
|
373
377
|
nextPage: page < totalPages ? page + 1 : null,
|
|
374
|
-
nextCursor: docs.length > 0 ? docs[docs.length - 1][cursorColumn] : null,
|
|
378
|
+
nextCursor: docs.length > 0 && hasNextPage ? docs[docs.length - 1][cursorColumn] : null,
|
|
375
379
|
prevCursor: docs.length > 0 && (page > 1 || !!options?.after || !!options?.before) ? docs[0][cursorColumn] : null
|
|
376
380
|
};
|
|
377
381
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface OpacaClientOptions {
|
|
2
2
|
baseURL: string;
|
|
3
3
|
token?: string;
|
|
4
|
+
api?: any;
|
|
4
5
|
}
|
|
5
6
|
export interface OpacaAsset {
|
|
6
7
|
id: string;
|
|
@@ -53,6 +54,8 @@ type MapFieldsToType<Fs extends readonly any[]> = {
|
|
|
53
54
|
[K in Fs[number] as K["required"] extends true ? never : K["name"]]?: MapFieldToType<K>;
|
|
54
55
|
};
|
|
55
56
|
type InferCollectionType<C> = C extends {
|
|
57
|
+
__type: infer T;
|
|
58
|
+
} ? T : C extends {
|
|
56
59
|
fields: infer Fs;
|
|
57
60
|
timestamps?: infer T;
|
|
58
61
|
} ? (Fs extends readonly any[] ? MapFieldsToType<Fs> : any) & (T extends true | {
|
|
@@ -63,6 +66,8 @@ type InferCollectionType<C> = C extends {
|
|
|
63
66
|
updatedAt: string;
|
|
64
67
|
} : {}) : any;
|
|
65
68
|
type InferGlobalType<G> = G extends {
|
|
69
|
+
__type: infer T;
|
|
70
|
+
} ? T : G extends {
|
|
66
71
|
fields: infer Fs;
|
|
67
72
|
} ? Fs extends readonly any[] ? MapFieldsToType<Fs> : any : any;
|
|
68
73
|
type InferCollections<T> = T extends {
|
package/dist/client.js
CHANGED
package/dist/config.d.ts
CHANGED
|
@@ -23,5 +23,12 @@ export declare function defineConfig<const TCollections extends readonly Buildab
|
|
|
23
23
|
}>[] = [], TResource extends string = ResourceOf<TCollections, TGlobals>>(config: Omit<OpacaConfig<TResource>, "collections" | "globals"> & {
|
|
24
24
|
collections: TCollections;
|
|
25
25
|
globals?: TGlobals;
|
|
26
|
-
}): OpacaConfig<TResource
|
|
26
|
+
}): Omit<OpacaConfig<TResource>, "collections" | "globals"> & {
|
|
27
|
+
collections: {
|
|
28
|
+
[K in keyof TCollections]: BuiltResource<TCollections[K]>;
|
|
29
|
+
};
|
|
30
|
+
globals: {
|
|
31
|
+
[K in keyof TGlobals]: BuiltResource<TGlobals[K]>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
27
34
|
export {};
|
package/dist/db/better-sqlite.js
CHANGED
package/dist/db/bun-sqlite.js
CHANGED
package/dist/db/d1.js
CHANGED
package/dist/db/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
D1Adapter,
|
|
3
3
|
createD1Adapter
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-5b9eqr34.js";
|
|
5
5
|
import {
|
|
6
6
|
PostgresAdapter,
|
|
7
7
|
createPostgresAdapter
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-tsmhn78f.js";
|
|
9
9
|
import {
|
|
10
10
|
SQLiteAdapter,
|
|
11
11
|
createSQLiteAdapter
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-nz6xhtja.js";
|
|
13
13
|
import {
|
|
14
14
|
BunSQLiteAdapter,
|
|
15
15
|
createBunSQLiteAdapter
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-qsefknd3.js";
|
|
17
17
|
import {
|
|
18
18
|
BetterSQLiteAdapter,
|
|
19
19
|
createBetterSQLiteAdapter
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-dz5bh1bd.js";
|
|
21
21
|
import"../chunk-re459gm9.js";
|
|
22
22
|
import {
|
|
23
23
|
BaseDatabaseAdapter
|
package/dist/db/postgres.js
CHANGED
package/dist/db/sqlite.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Faker } from "@faker-js/faker";
|
|
2
|
-
import type { AccessConfig
|
|
2
|
+
import type { AccessConfig } from "../types/access";
|
|
3
|
+
import type { CollectionHooks, LucideIcons } from "@/types";
|
|
3
4
|
import { type ZodObject, type ZodRawShape, type infer as zInfer } from "./zod";
|
|
4
|
-
export interface CollectionConfig<T = any> {
|
|
5
|
-
slug:
|
|
5
|
+
export interface CollectionConfig<T = any, S extends string = string> {
|
|
6
|
+
slug: S;
|
|
6
7
|
label?: string;
|
|
7
8
|
admin?: {
|
|
8
9
|
useAsTitle?: string;
|
|
@@ -20,7 +21,7 @@ export interface CollectionConfig<T = any> {
|
|
|
20
21
|
createdAt?: string;
|
|
21
22
|
updatedAt?: string;
|
|
22
23
|
};
|
|
23
|
-
access?: AccessConfig
|
|
24
|
+
access?: AccessConfig<T>;
|
|
24
25
|
hooks?: CollectionHooks<T>;
|
|
25
26
|
versions?: {
|
|
26
27
|
drafts?: boolean;
|
|
@@ -58,19 +59,19 @@ export interface CollectionConfig<T = any> {
|
|
|
58
59
|
*/
|
|
59
60
|
seed?: (faker: Faker) => Partial<T> | Promise<Partial<T>>;
|
|
60
61
|
}
|
|
61
|
-
export interface DefineCollectionArgs<T extends ZodRawShape> extends CollectionConfig<zInfer<ZodObject<T
|
|
62
|
+
export interface DefineCollectionArgs<T extends ZodRawShape, S extends string> extends CollectionConfig<zInfer<ZodObject<T>>, S> {
|
|
62
63
|
schema: ZodObject<T>;
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
65
66
|
* Replaces the legacy Collection.create() Builder API.
|
|
66
67
|
* Wraps a Zod Schema and attaches collection-level metadata to it.
|
|
67
68
|
*/
|
|
68
|
-
export declare function defineCollection<T extends ZodRawShape>(config: DefineCollectionArgs<T>): {
|
|
69
|
+
export declare function defineCollection<T extends ZodRawShape, S extends string>(config: DefineCollectionArgs<T, S>): {
|
|
69
70
|
schema: ZodObject<T, import("better-auth").$strip>;
|
|
70
71
|
fields: any[];
|
|
71
72
|
seed: ((faker: Faker) => Partial<import("better-auth").$InferObjectOutput<T, {}>> | Promise<Partial<import("better-auth").$InferObjectOutput<T, {}>>>) | undefined;
|
|
72
73
|
__type: zInfer<ZodObject<T, import("better-auth").$strip>>;
|
|
73
|
-
slug:
|
|
74
|
+
slug: S;
|
|
74
75
|
label?: string;
|
|
75
76
|
admin?: {
|
|
76
77
|
useAsTitle?: string;
|
|
@@ -88,7 +89,7 @@ export declare function defineCollection<T extends ZodRawShape>(config: DefineCo
|
|
|
88
89
|
createdAt?: string;
|
|
89
90
|
updatedAt?: string;
|
|
90
91
|
};
|
|
91
|
-
access?: AccessConfig;
|
|
92
|
+
access?: AccessConfig<import("better-auth").$InferObjectOutput<T, {}>> | undefined;
|
|
92
93
|
hooks?: CollectionHooks<import("better-auth").$InferObjectOutput<T, {}>> | undefined;
|
|
93
94
|
versions?: {
|
|
94
95
|
drafts?: boolean;
|
package/dist/schema/global.d.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import type { Faker } from "@faker-js/faker";
|
|
2
|
+
import type { AccessConfig } from "../types/access";
|
|
2
3
|
import { type ZodObject, type ZodRawShape, type infer as zInfer } from "./zod";
|
|
3
|
-
export interface GlobalConfig {
|
|
4
|
-
slug:
|
|
4
|
+
export interface GlobalConfig<T = any, S extends string = string> {
|
|
5
|
+
slug: S;
|
|
5
6
|
label?: string;
|
|
6
7
|
admin?: {
|
|
7
8
|
hidden?: boolean;
|
|
8
9
|
group?: string;
|
|
9
10
|
icon?: string;
|
|
10
11
|
};
|
|
11
|
-
access?:
|
|
12
|
-
read?: (context: any) => boolean | Promise<boolean>;
|
|
13
|
-
update?: (context: any) => boolean | Promise<boolean>;
|
|
14
|
-
};
|
|
12
|
+
access?: AccessConfig<T>;
|
|
15
13
|
/**
|
|
16
14
|
* Seed function for the global.
|
|
17
15
|
* @param faker faker instance
|
|
@@ -19,26 +17,23 @@ export interface GlobalConfig {
|
|
|
19
17
|
*/
|
|
20
18
|
seed?: (faker: Faker) => any | Promise<any>;
|
|
21
19
|
}
|
|
22
|
-
export interface DefineGlobalArgs<T extends ZodRawShape> extends GlobalConfig {
|
|
20
|
+
export interface DefineGlobalArgs<T extends ZodRawShape, S extends string> extends GlobalConfig<zInfer<ZodObject<T>>, S> {
|
|
23
21
|
schema: ZodObject<T>;
|
|
24
22
|
}
|
|
25
23
|
/**
|
|
26
24
|
* Zod-based replacement for the legacy GlobalBuilder.
|
|
27
25
|
*/
|
|
28
|
-
export declare function defineGlobal<T extends ZodRawShape>(config: DefineGlobalArgs<T>): {
|
|
26
|
+
export declare function defineGlobal<T extends ZodRawShape, S extends string>(config: DefineGlobalArgs<T, S>): {
|
|
29
27
|
schema: ZodObject<T, import("better-auth").$strip>;
|
|
30
28
|
fields: any[];
|
|
31
29
|
seed: ((faker: Faker) => any | Promise<any>) | undefined;
|
|
32
30
|
__type: zInfer<ZodObject<T, import("better-auth").$strip>>;
|
|
33
|
-
slug:
|
|
31
|
+
slug: S;
|
|
34
32
|
label?: string;
|
|
35
33
|
admin?: {
|
|
36
34
|
hidden?: boolean;
|
|
37
35
|
group?: string;
|
|
38
36
|
icon?: string;
|
|
39
37
|
};
|
|
40
|
-
access?: {
|
|
41
|
-
read?: (context: any) => boolean | Promise<boolean>;
|
|
42
|
-
update?: (context: any) => boolean | Promise<boolean>;
|
|
43
|
-
};
|
|
38
|
+
access?: AccessConfig<import("better-auth").$InferObjectOutput<T, {}>> | undefined;
|
|
44
39
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
export interface AccessArgs<T = any> {
|
|
3
|
+
req: Context;
|
|
4
|
+
user: any;
|
|
5
|
+
session: any;
|
|
6
|
+
apiKey: any;
|
|
7
|
+
/** The document being created/updated or the search query */
|
|
8
|
+
data?: T;
|
|
9
|
+
/** The ID of the document being read/updated/deleted */
|
|
10
|
+
id?: string;
|
|
11
|
+
operation: "read" | "create" | "update" | "delete";
|
|
12
|
+
}
|
|
13
|
+
export interface AccessConfig<T = any> {
|
|
14
|
+
read?: boolean | ((args: AccessArgs<T>) => boolean | Promise<boolean>);
|
|
15
|
+
create?: boolean | ((args: AccessArgs<T>) => boolean | Promise<boolean>);
|
|
16
|
+
update?: boolean | ((args: AccessArgs<T>) => boolean | Promise<boolean>);
|
|
17
|
+
delete?: boolean | ((args: AccessArgs<T>) => boolean | Promise<boolean>);
|
|
18
|
+
requireApiKey?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface FieldAccessArgs extends AccessArgs {
|
|
21
|
+
field: string;
|
|
22
|
+
}
|
|
23
|
+
export interface FieldAccessConfig {
|
|
24
|
+
/** If true, the field is removed from API and UI. */
|
|
25
|
+
hidden?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
26
|
+
/** If true, the data is shown but cannot be edited. */
|
|
27
|
+
readOnly?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
28
|
+
/** If true, the input is visually disabled. */
|
|
29
|
+
disabled?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
30
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Context } from "hono";
|
|
|
4
4
|
import type { icons } from "lucide-react";
|
|
5
5
|
import type { z } from "zod";
|
|
6
6
|
import type { AdminConfig, FieldType } from "./validation";
|
|
7
|
+
import type { AccessArgs, AccessConfig, FieldAccessArgs, FieldAccessConfig } from "./types/access";
|
|
8
|
+
export type { AccessArgs, AccessConfig, FieldAccessArgs, FieldAccessConfig };
|
|
7
9
|
export type { FieldType };
|
|
8
10
|
export type Session = BetterAuthSession;
|
|
9
11
|
export type User = Prettify<BetterAuthUser & {
|
|
@@ -273,15 +275,6 @@ export interface ApiKey {
|
|
|
273
275
|
permissions?: Record<string, string[]> | null;
|
|
274
276
|
referenceId: string;
|
|
275
277
|
}
|
|
276
|
-
export interface AccessArgs {
|
|
277
|
-
req: Context;
|
|
278
|
-
user: User | null;
|
|
279
|
-
session: Session | null;
|
|
280
|
-
apiKey?: ApiKey | null;
|
|
281
|
-
data?: any;
|
|
282
|
-
id?: string;
|
|
283
|
-
operation: "read" | "create" | "update" | "delete";
|
|
284
|
-
}
|
|
285
278
|
export interface CollectionHookArgs<T = any> {
|
|
286
279
|
req: Context;
|
|
287
280
|
user: User | null;
|
|
@@ -293,21 +286,6 @@ export interface CollectionHookArgs<T = any> {
|
|
|
293
286
|
id?: string;
|
|
294
287
|
operation: "create" | "update" | "delete";
|
|
295
288
|
}
|
|
296
|
-
export interface FieldAccessConfig {
|
|
297
|
-
/** If true, the field is removed from API and UI. */
|
|
298
|
-
hidden?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
299
|
-
/** If true, the data is shown but cannot be edited. */
|
|
300
|
-
readOnly?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
301
|
-
/** If true, the input is visually disabled. */
|
|
302
|
-
disabled?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
303
|
-
}
|
|
304
|
-
export interface AccessConfig {
|
|
305
|
-
read?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
306
|
-
create?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
307
|
-
update?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
308
|
-
delete?: boolean | ((args: AccessArgs) => boolean | Promise<boolean>);
|
|
309
|
-
requireApiKey?: boolean;
|
|
310
|
-
}
|
|
311
289
|
export interface CollectionHooks<T = any> {
|
|
312
290
|
beforeCreate?: (args: CollectionHookArgs<T>) => T | Promise<T> | void;
|
|
313
291
|
afterCreate?: (args: CollectionHookArgs<T>) => void | Promise<void>;
|
|
@@ -371,7 +349,7 @@ export interface Collection<T = any> {
|
|
|
371
349
|
/** Lifecycle hooks for custom logic before/after database operations. */
|
|
372
350
|
hooks?: CollectionHooks<T>;
|
|
373
351
|
/** Access control configuration for the collection. */
|
|
374
|
-
access?: AccessConfig
|
|
352
|
+
access?: AccessConfig<T>;
|
|
375
353
|
/** Versioning and draft support configuration. */
|
|
376
354
|
versions?: {
|
|
377
355
|
/** Enable draft/published states for records. */
|
|
@@ -426,10 +404,10 @@ export interface Collection<T = any> {
|
|
|
426
404
|
hidden?: boolean;
|
|
427
405
|
seed?: (faker: Faker) => Partial<T> | Promise<Partial<T>>;
|
|
428
406
|
}
|
|
429
|
-
export interface Global {
|
|
407
|
+
export interface Global<T = any> {
|
|
430
408
|
slug: string;
|
|
431
409
|
fields: Field[];
|
|
432
|
-
access?: AccessConfig
|
|
410
|
+
access?: AccessConfig<T>;
|
|
433
411
|
label?: string;
|
|
434
412
|
icon?: LucideIcons;
|
|
435
413
|
timestamps?: boolean | {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opacacms",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "OpacaCMS: A lightweight, type-safe, and developer-first Headless CMS for the edge and beyond.",
|
|
6
6
|
"keywords": [
|
|
@@ -231,6 +231,7 @@
|
|
|
231
231
|
"zod-to-json-schema": "^3.25.1"
|
|
232
232
|
},
|
|
233
233
|
"files": [
|
|
234
|
-
"dist"
|
|
234
|
+
"dist",
|
|
235
|
+
"LICENSE"
|
|
235
236
|
]
|
|
236
237
|
}
|