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 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 { Collection, Field } from 'opacacms';
127
-
128
- export const posts = Collection.create('posts')
129
- .label('Blog Posts')
130
- .icon('FileText')
131
- .fields([
132
- Field.text('title').required().label('Post Title'),
133
- Field.slug('slug').from('title').unique(),
134
- Field.richText('content').localized(),
135
- Field.relationship('author').to('_users').single(),
136
- Field.select('status')
137
- .options([
138
- { label: 'Draft', value: 'draft' },
139
- { label: 'Published', value: 'published' },
140
- ])
141
- .defaultValue('draft'),
142
- Field.checkbox('featured').label('Featured Post'),
143
- ])
144
- .access({
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
- .hooks({
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 Builder Methods
163
+ ### Collection Configuration
162
164
 
163
- | Method | Description |
164
- | -------------------- | ---------------------------------------------------------------- |
165
- | `.label(name)` | Sets the display name used in the Admin UI sidebar |
166
- | `.icon(name)` | [Lucide](https://lucide.dev) icon name for the sidebar |
167
- | `.fields([...])` | Defines the data structure for this collection |
168
- | `.access(rules)` | Collection-level access control |
169
- | `.hooks(fns)` | Lifecycle hooks (`beforeCreate`, `afterUpdate`, etc.) |
170
- | `.webhooks([...])` | External webhook notifications |
171
- | `.admin({...})` | Advanced Admin UI configuration (`hidden`, `disableAdmin`, etc.) |
172
- | `.versions(true)` | Enable document versioning with history |
173
- | `.timestamps({...})` | Customize timestamp field names |
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
- We've got everything you need to build powerful schemas:
180
-
181
- | Field | Usage | Description |
182
- | ---------------------- | ------------------------------------------- | --------------------------------------------- |
183
- | `Field.text()` | `Field.text('title')` | Simple string input |
184
- | `Field.number()` | `Field.number('price')` | Numeric input |
185
- | `Field.richText()` | `Field.richText('content')` | Block-based Lexical editor (Notion style!) 📝 |
186
- | `Field.relationship()` | `Field.relationship('author').to('_users')` | Links to another collection |
187
- | `Field.file()` | `Field.file('image')` | File/image upload ☁️ |
188
- | `Field.blocks()` | `Field.blocks('layout').blocks([...])` | Dynamic page builder 🧱 |
189
- | `Field.group()` | `Field.group('meta').fields([...])` | Nested object group |
190
- | `Field.array()` | `Field.array('tags').fields([...])` | Repeatable field group |
191
- | `Field.select()` | `Field.select('status').options([...])` | Dropdown picker |
192
- | `Field.checkbox()` | `Field.checkbox('active')` | Boolean toggle |
193
- | `Field.slug()` | `Field.slug('slug').from('title')` | Auto-generated URL slug |
194
- | `Field.date()` | `Field.date('publishedAt')` | Date/time picker |
195
- | `Field.virtual()` | `Field.virtual('fullName').resolve(...)` | Computed field (not stored) |
196
- | `Field.tabs()` | `Field.tabs('layout').tabs([...])` | UI-only grouping for the admin |
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
- Field.text('email')
204
- .label('Email Address') // Admin UI label
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
- .defaultValue('hello@world.com') // Default value
210
- .validate(z.string().email()) // Custom validation (function or Zod)
211
- .access({ readOnly: true }) // Field-level access control
212
- .description('Primary email') // Help text below the field
213
- .hidden() // Hide from Admin UI
214
- .readOnly() // Read-only in Admin UI
215
- .admin({ components: { Field: 'my-custom-field' } }); // Custom component
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 **granular field validation** via the `.validate()` method. You can pass either a **custom function** or a **Zod schema** they're fully interchangeable.
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
- Pass any `z.ZodTypeAny` schema directly. Errors are automatically mapped:
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
- Field.text('cpf').validate(
243
- z.string().regex(/^\d{3}\.\d{3}\.\d{3}-\d{2}$/, 'Invalid CPF format'),
244
- );
233
+ // Regular expressions
234
+ z.text().regex(/^\d{3}\.\d{3}\.\d{3}-\d{2}$/, 'Invalid CPF format');
245
235
 
246
- Field.text('password').validate(
247
- z
248
- .string()
249
- .min(8, 'Password must be at least 8 characters')
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
- Field.text('email')
254
- .required()
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 { Global, Field } from 'opacacms';
254
+ import { defineGlobal, z } from 'opacacms';
268
255
 
269
- export const siteSettings = Global.create('site-settings')
270
- .label('Site Settings')
271
- .icon('Settings')
272
- .fields([
273
- Field.text('siteName').required(),
274
- Field.text('tagline').localized(),
275
- Field.file('logo'),
276
- Field.group('social').fields([Field.text('twitter'), Field.text('github')]),
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
- .access({
292
- read: ({ user }) => !!user, // Logged in? You're good.
293
- create: ({ user }) => user?.role === 'admin', // Only admins please!
294
- update: ({ data, user }) => data.ownerId === user.id, // Only your own stuff.
295
- delete: ({ user }) => user?.role === 'admin',
296
- requireApiKey: true, // Require API key for programmatic access
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
- Field.text('internalNotes').access({
306
- hidden: ({ user }) => user?.role !== 'admin', // Only admins see this
307
- readOnly: ({ operation }) => operation === 'update', // Editable only on create
308
- disabled: false,
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
- .hooks({
338
- beforeCreate: async (data) => {
339
- // Transform or enrich data before saving
340
- data.slug = data.title.toLowerCase().replace(/\s+/g, '-');
341
- return data;
342
- },
343
- afterCreate: async (doc) => {
344
- // Side-effects after the document is saved
345
- await sendWelcomeEmail(doc.email);
346
- },
347
- beforeUpdate: async (data) => {
348
- data.updatedBy = 'system';
349
- return data;
350
- },
351
- afterUpdate: async (doc) => {
352
- await invalidateCache(`/posts/${doc.slug}`);
353
- },
354
- beforeDelete: async (id) => {
355
- await archiveDocument(id);
356
- },
357
- afterDelete: async (id) => {
358
- console.log(`Document ${id} deleted`);
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
- url: 'https://hooks.slack.com/services/xxx',
377
- events: ['afterCreate', 'afterUpdate'],
378
- headers: { Authorization: 'Bearer my-token' },
379
- },
380
- {
381
- url: 'https://api.example.com/webhooks/orders',
382
- events: ['afterDelete'],
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: `afterCreate`, `afterUpdate`, `afterDelete`.
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
- Collection.create('posts')
397
- .versions(true) // That's it!
398
- .fields([...])
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
- import { Field } from 'opacacms';
423
+ const userShape = z.object({
424
+ firstName: z.text(),
425
+ lastName: z.text(),
426
+ });
418
427
 
419
- Field.virtual('fullName').resolve(async ({ data, user, req }) => {
420
- return `${data.firstName} ${data.lastName}`;
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. All adapters implement the same interface, so switching is as simple as changing one line.
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
- Field.text('title').localized()
556
- Field.richText('content').localized()
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
- Field.text('color').admin({
614
- components: {
615
- Field: 'my-color-picker',
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 = Collection.create('internal_data')
639
- .admin({
661
+ export const InternalData = defineCollection({
662
+ slug: 'internal_data',
663
+ admin: {
640
664
  hidden: true, // Only accessible via direct link
641
- })
642
- .fields([...]);
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(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: page * limit < total,
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: page * limit < total,
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 options = configOrOptions.serverURL ? { baseURL: configOrOptions.serverURL } : configOrOptions;
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
- engineConfig = { rest: true, graphql: { enabled: false, path: "/graphql" } };
59
- metadataCache = { collections: [], globals: [] };
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
- return fetcher(engineConfig.graphql.path, {
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: page * limit < total,
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: page * limit < total,
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: page * limit < total,
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  OpacaError,
3
3
  createClient
4
- } from "./chunk-gj8w1r1e.js";
4
+ } from "./chunk-nv91gc63.js";
5
5
  import"./chunk-2vbfc4q8.js";
6
6
  import"./chunk-8sqjbsgt.js";
7
7
  export {
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 {};
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BetterSQLiteAdapter,
3
3
  createBetterSQLiteAdapter
4
- } from "../chunk-1vtdkx5e.js";
4
+ } from "../chunk-dz5bh1bd.js";
5
5
  import"../chunk-re459gm9.js";
6
6
  import"../chunk-s8mqwnm1.js";
7
7
  import"../chunk-q5sb5dcr.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BunSQLiteAdapter,
3
3
  createBunSQLiteAdapter
4
- } from "../chunk-0njhbe4a.js";
4
+ } from "../chunk-qsefknd3.js";
5
5
  import"../chunk-re459gm9.js";
6
6
  import"../chunk-s8mqwnm1.js";
7
7
  import"../chunk-q5sb5dcr.js";
package/dist/db/d1.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  D1Adapter,
3
3
  createD1Adapter
4
- } from "../chunk-8hhzvesq.js";
4
+ } from "../chunk-5b9eqr34.js";
5
5
  import"../chunk-re459gm9.js";
6
6
  import"../chunk-s8mqwnm1.js";
7
7
  import"../chunk-q5sb5dcr.js";
package/dist/db/index.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import {
2
2
  D1Adapter,
3
3
  createD1Adapter
4
- } from "../chunk-8hhzvesq.js";
4
+ } from "../chunk-5b9eqr34.js";
5
5
  import {
6
6
  PostgresAdapter,
7
7
  createPostgresAdapter
8
- } from "../chunk-2ec9fsgr.js";
8
+ } from "../chunk-tsmhn78f.js";
9
9
  import {
10
10
  SQLiteAdapter,
11
11
  createSQLiteAdapter
12
- } from "../chunk-xdmw9vzq.js";
12
+ } from "../chunk-nz6xhtja.js";
13
13
  import {
14
14
  BunSQLiteAdapter,
15
15
  createBunSQLiteAdapter
16
- } from "../chunk-0njhbe4a.js";
16
+ } from "../chunk-qsefknd3.js";
17
17
  import {
18
18
  BetterSQLiteAdapter,
19
19
  createBetterSQLiteAdapter
20
- } from "../chunk-1vtdkx5e.js";
20
+ } from "../chunk-dz5bh1bd.js";
21
21
  import"../chunk-re459gm9.js";
22
22
  import {
23
23
  BaseDatabaseAdapter
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PostgresAdapter,
3
3
  createPostgresAdapter
4
- } from "../chunk-2ec9fsgr.js";
4
+ } from "../chunk-tsmhn78f.js";
5
5
  import"../chunk-re459gm9.js";
6
6
  import"../chunk-s8mqwnm1.js";
7
7
  import"../chunk-q5sb5dcr.js";
package/dist/db/sqlite.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SQLiteAdapter,
3
3
  createSQLiteAdapter
4
- } from "../chunk-xdmw9vzq.js";
4
+ } from "../chunk-nz6xhtja.js";
5
5
  import"../chunk-re459gm9.js";
6
6
  import"../chunk-s8mqwnm1.js";
7
7
  import"../chunk-q5sb5dcr.js";
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ export * from "./config";
4
4
  export * from "./config-utils";
5
5
  export * from "./schema";
6
6
  export * from "./types";
7
- export type { AdminConfig, CollectionHooks, FieldType, } from "./validation";
7
+ export type { AdminConfig, } from "./validation";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  OpacaError,
3
3
  createClient
4
- } from "./chunk-gj8w1r1e.js";
4
+ } from "./chunk-nv91gc63.js";
5
5
  import {
6
6
  defineCollection,
7
7
  defineGlobal
@@ -1,8 +1,9 @@
1
1
  import type { Faker } from "@faker-js/faker";
2
- import type { AccessConfig, CollectionHooks, LucideIcons } from "@/types";
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: string;
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: string;
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;
@@ -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: string;
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: string;
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.13",
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
  }