opacacms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +34 -0
- package/dist/admin/api-client.d.ts +8 -0
- package/dist/admin/auth-client.d.ts +940 -0
- package/dist/admin/custom-field.d.ts +71 -0
- package/dist/admin/index.d.ts +11 -0
- package/dist/admin/react.d.ts +3 -0
- package/dist/admin/router.d.ts +7 -0
- package/dist/admin/stores/admin-queries.d.ts +32 -0
- package/dist/admin/stores/auth.d.ts +33 -0
- package/dist/admin/stores/column-visibility.d.ts +21 -0
- package/dist/admin/stores/config.d.ts +7 -0
- package/dist/admin/stores/media.d.ts +44 -0
- package/dist/admin/stores/query.d.ts +4 -0
- package/dist/admin/stores/ui.d.ts +11 -0
- package/dist/admin/ui/admin-client.d.ts +7 -0
- package/dist/admin/ui/admin-layout.d.ts +14 -0
- package/dist/admin/ui/components/ColumnVisibilityToggle.d.ts +10 -0
- package/dist/admin/ui/components/DataDetailSheet.d.ts +13 -0
- package/dist/admin/ui/components/DataDetailView.d.ts +9 -0
- package/dist/admin/ui/components/Table.d.ts +10 -0
- package/dist/admin/ui/components/fields/ArrayField.d.ts +13 -0
- package/dist/admin/ui/components/fields/BlocksField.d.ts +17 -0
- package/dist/admin/ui/components/fields/BooleanField.d.ts +13 -0
- package/dist/admin/ui/components/fields/CollapsibleField.d.ts +16 -0
- package/dist/admin/ui/components/fields/DateField.d.ts +13 -0
- package/dist/admin/ui/components/fields/FileField.d.ts +23 -0
- package/dist/admin/ui/components/fields/GroupField.d.ts +13 -0
- package/dist/admin/ui/components/fields/JoinField.d.ts +15 -0
- package/dist/admin/ui/components/fields/NumberField.d.ts +14 -0
- package/dist/admin/ui/components/fields/RadioField.d.ts +17 -0
- package/dist/admin/ui/components/fields/RelationshipField.d.ts +16 -0
- package/dist/admin/ui/components/fields/RowField.d.ts +12 -0
- package/dist/admin/ui/components/fields/SelectField.d.ts +18 -0
- package/dist/admin/ui/components/fields/TabsField.d.ts +15 -0
- package/dist/admin/ui/components/fields/TextAreaField.d.ts +14 -0
- package/dist/admin/ui/components/fields/TextField.d.ts +14 -0
- package/dist/admin/ui/components/fields/VirtualField.d.ts +8 -0
- package/dist/admin/ui/components/fields/index.d.ts +28 -0
- package/dist/admin/ui/components/fields/richtext-editor/index.d.ts +10 -0
- package/dist/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.d.ts +7 -0
- package/dist/admin/ui/components/fields/richtext-editor/nodes/ImageNode.d.ts +27 -0
- package/dist/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.d.ts +1 -0
- package/dist/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.d.ts +5 -0
- package/dist/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.d.ts +1 -0
- package/dist/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.d.ts +1 -0
- package/dist/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.d.ts +5 -0
- package/dist/admin/ui/components/fields/utils.d.ts +1 -0
- package/dist/admin/ui/components/link.d.ts +8 -0
- package/dist/admin/ui/components/media/AssetManagerModal.d.ts +17 -0
- package/dist/admin/ui/components/toast.d.ts +10 -0
- package/dist/admin/ui/components/ui/accordion.d.ts +11 -0
- package/dist/admin/ui/components/ui/alert-dialog.d.ts +12 -0
- package/dist/admin/ui/components/ui/blocks.d.ts +5 -0
- package/dist/admin/ui/components/ui/breadcrumbs.d.ts +7 -0
- package/dist/admin/ui/components/ui/button.d.ts +7 -0
- package/dist/admin/ui/components/ui/collapsible.d.ts +16 -0
- package/dist/admin/ui/components/ui/dialog.d.ts +27 -0
- package/dist/admin/ui/components/ui/group.d.ts +6 -0
- package/dist/admin/ui/components/ui/index.d.ts +17 -0
- package/dist/admin/ui/components/ui/input.d.ts +5 -0
- package/dist/admin/ui/components/ui/join.d.ts +7 -0
- package/dist/admin/ui/components/ui/label.d.ts +3 -0
- package/dist/admin/ui/components/ui/radio-group.d.ts +13 -0
- package/dist/admin/ui/components/ui/relationship-detail-sheet.d.ts +9 -0
- package/dist/admin/ui/components/ui/relationship.d.ts +8 -0
- package/dist/admin/ui/components/ui/scroll-area.d.ts +7 -0
- package/dist/admin/ui/components/ui/select.d.ts +37 -0
- package/dist/admin/ui/components/ui/separator.d.ts +8 -0
- package/dist/admin/ui/components/ui/sheet.d.ts +28 -0
- package/dist/admin/ui/components/ui/tabs.d.ts +17 -0
- package/dist/admin/ui/components/ui/utils.d.ts +1 -0
- package/dist/admin/ui/hooks/use-debounce.d.ts +1 -0
- package/dist/admin/ui/views/collection-list-view.d.ts +5 -0
- package/dist/admin/ui/views/dashboard-view.d.ts +10 -0
- package/dist/admin/ui/views/document-edit-view.d.ts +7 -0
- package/dist/admin/ui/views/global-edit-view.d.ts +19 -0
- package/dist/admin/ui/views/init-view.d.ts +4 -0
- package/dist/admin/ui/views/login-view.d.ts +4 -0
- package/dist/admin/ui/views/media-registry-view.d.ts +7 -0
- package/dist/admin/ui/views/settings-view.d.ts +7 -0
- package/dist/admin/webcomponent.d.ts +1 -0
- package/dist/api.d.ts +6 -0
- package/dist/auth/index.d.ts +2107 -0
- package/dist/auth/migrations.d.ts +5 -0
- package/dist/auth/premissions.d.ts +6 -0
- package/dist/chunk-16vgcf3k.js +88 -0
- package/dist/chunk-2zm8cy1w.js +9482 -0
- package/dist/chunk-5gvbp2qa.js +167 -0
- package/dist/chunk-62ev8gnc.js +41 -0
- package/dist/chunk-6dhs73zq.js +126 -0
- package/dist/chunk-6ew02s0c.js +472 -0
- package/dist/chunk-7a9kn0np.js +116 -0
- package/dist/chunk-8gkhn1d4.js +309 -0
- package/dist/chunk-8sqjbsgt.js +42 -0
- package/dist/chunk-9kxpbcb1.js +85 -0
- package/dist/chunk-cvdd4eqh.js +110 -0
- package/dist/chunk-d3ffeqp9.js +87 -0
- package/dist/chunk-dy5t83hr.js +261 -0
- package/dist/chunk-f3nvxn63.js +17 -0
- package/dist/chunk-hmhcense.js +1352 -0
- package/dist/chunk-j4d50hrx.js +20 -0
- package/dist/chunk-jwjk85ze.js +15 -0
- package/dist/chunk-kwp83w8b.js +250 -0
- package/dist/chunk-s8mqwnm1.js +14 -0
- package/dist/chunk-srsac177.js +85 -0
- package/dist/chunk-v521d72w.js +10 -0
- package/dist/chunk-xa7rjsn2.js +20 -0
- package/dist/chunk-xg35h5a3.js +15 -0
- package/dist/chunk-ybbbqj63.js +130 -0
- package/dist/chunk-zvwb67nd.js +332 -0
- package/dist/cli/commands/generate-types.d.ts +1 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/migrate-commands.d.ts +5 -0
- package/dist/cli/commands/seed-command.d.ts +2 -0
- package/dist/cli/d1-mock.d.ts +30 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.test.d.ts +1 -0
- package/dist/cli/r2-mock.d.ts +46 -0
- package/dist/cli/seeding.d.ts +17 -0
- package/dist/client.d.ts +51 -0
- package/dist/config-utils.d.ts +6 -0
- package/dist/config.d.ts +10 -0
- package/dist/db/adapter.d.ts +34 -0
- package/dist/db/better-sqlite.d.ts +40 -0
- package/dist/db/bun-sqlite.d.ts +40 -0
- package/dist/db/d1.d.ts +42 -0
- package/dist/db/kysely/data-mapper.d.ts +6 -0
- package/dist/db/kysely/field-mapper.d.ts +22 -0
- package/dist/db/kysely/migration-generator.d.ts +9 -0
- package/dist/db/kysely/query-builder.d.ts +9 -0
- package/dist/db/kysely/schema-builder.d.ts +15 -0
- package/dist/db/kysely/sql-utils.d.ts +1 -0
- package/dist/db/postgres.d.ts +51 -0
- package/dist/db/sqlite.d.ts +41 -0
- package/dist/db/system-schema.d.ts +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/runtimes/bun.d.ts +17 -0
- package/dist/runtimes/cloudflare-workers.d.ts +10 -0
- package/dist/runtimes/next.d.ts +16 -0
- package/dist/runtimes/node.d.ts +18 -0
- package/dist/schema/collection.d.ts +100 -0
- package/dist/schema/fields/base.d.ts +83 -0
- package/dist/schema/fields/index.d.ts +135 -0
- package/dist/schema/global.d.ts +82 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/infer.d.ts +55 -0
- package/dist/server/admin-router.d.ts +9 -0
- package/dist/server/admin.d.ts +18 -0
- package/dist/server/assets.d.ts +47 -0
- package/dist/server/collection-router.d.ts +14 -0
- package/dist/server/handlers.d.ts +76 -0
- package/dist/server/middlewares/admin.d.ts +6 -0
- package/dist/server/middlewares/auth.d.ts +16 -0
- package/dist/server/middlewares/context.d.ts +9 -0
- package/dist/server/middlewares/cors.d.ts +3 -0
- package/dist/server/middlewares/database-init.d.ts +11 -0
- package/dist/server/middlewares/rate-limit.d.ts +3 -0
- package/dist/server/router.d.ts +7 -0
- package/dist/server/setup-middlewares.d.ts +17 -0
- package/dist/server/system-router.d.ts +9 -0
- package/dist/server.d.ts +6 -0
- package/dist/src/admin/index.css +47 -0
- package/dist/src/admin/index.js +176 -0
- package/dist/src/admin/webcomponent.js +19 -0
- package/dist/src/api.js +27 -0
- package/dist/src/cli/index.js +157 -0
- package/dist/src/client.js +9 -0
- package/dist/src/db/bun-sqlite.js +523 -0
- package/dist/src/db/d1.js +568 -0
- package/dist/src/db/postgres.js +520 -0
- package/dist/src/db/sqlite.js +534 -0
- package/dist/src/index.js +20 -0
- package/dist/src/runtimes/bun.js +36 -0
- package/dist/src/runtimes/cloudflare-workers.js +29 -0
- package/dist/src/runtimes/next.js +26 -0
- package/dist/src/runtimes/node.js +38 -0
- package/dist/src/server.js +27 -0
- package/dist/src/storage/index.js +355 -0
- package/dist/storage/adapters/cloudflare-r2.d.ts +6 -0
- package/dist/storage/adapters/local.d.ts +6 -0
- package/dist/storage/adapters/s3.d.ts +13 -0
- package/dist/storage/errors.d.ts +12 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/types.d.ts +31 -0
- package/dist/types.d.ts +484 -0
- package/dist/utils/lexical.d.ts +5 -0
- package/dist/utils/logger.d.ts +35 -0
- package/dist/validation.d.ts +300 -0
- package/dist/validator.d.ts +9 -0
- package/global.d.ts +11 -0
- package/package.json +151 -0
- package/src/admin/api-client.ts +63 -0
- package/src/admin/auth-client.ts +40 -0
- package/src/admin/custom-field.ts +179 -0
- package/src/admin/index.ts +15 -0
- package/src/admin/react.tsx +72 -0
- package/src/admin/router.ts +9 -0
- package/src/admin/stores/admin-queries.ts +121 -0
- package/src/admin/stores/auth.ts +61 -0
- package/src/admin/stores/column-visibility.ts +67 -0
- package/src/admin/stores/config.ts +15 -0
- package/src/admin/stores/media.ts +95 -0
- package/src/admin/stores/query.ts +13 -0
- package/src/admin/stores/ui.ts +29 -0
- package/src/admin/ui/admin-client.tsx +283 -0
- package/src/admin/ui/admin-layout.tsx +276 -0
- package/src/admin/ui/components/ColumnVisibilityToggle.tsx +141 -0
- package/src/admin/ui/components/DataDetailSheet.tsx +141 -0
- package/src/admin/ui/components/DataDetailView.tsx +175 -0
- package/src/admin/ui/components/Table.tsx +67 -0
- package/src/admin/ui/components/fields/ArrayField.tsx +166 -0
- package/src/admin/ui/components/fields/BlocksField.tsx +202 -0
- package/src/admin/ui/components/fields/BooleanField.tsx +50 -0
- package/src/admin/ui/components/fields/CollapsibleField.tsx +75 -0
- package/src/admin/ui/components/fields/DateField.tsx +45 -0
- package/src/admin/ui/components/fields/FileField.tsx +322 -0
- package/src/admin/ui/components/fields/GroupField.tsx +50 -0
- package/src/admin/ui/components/fields/JoinField.tsx +23 -0
- package/src/admin/ui/components/fields/NumberField.tsx +46 -0
- package/src/admin/ui/components/fields/RadioField.tsx +62 -0
- package/src/admin/ui/components/fields/RelationshipField.tsx +278 -0
- package/src/admin/ui/components/fields/RowField.tsx +40 -0
- package/src/admin/ui/components/fields/SelectField.tsx +59 -0
- package/src/admin/ui/components/fields/TabsField.tsx +101 -0
- package/src/admin/ui/components/fields/TextAreaField.tsx +54 -0
- package/src/admin/ui/components/fields/TextField.tsx +49 -0
- package/src/admin/ui/components/fields/VirtualField.tsx +53 -0
- package/src/admin/ui/components/fields/index.tsx +371 -0
- package/src/admin/ui/components/fields/richtext-editor/index.tsx +211 -0
- package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +142 -0
- package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +95 -0
- package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +226 -0
- package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +16 -0
- package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +184 -0
- package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +240 -0
- package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +40 -0
- package/src/admin/ui/components/fields/utils.ts +1 -0
- package/src/admin/ui/components/link.tsx +41 -0
- package/src/admin/ui/components/media/AssetManagerModal.tsx +334 -0
- package/src/admin/ui/components/toast.tsx +72 -0
- package/src/admin/ui/components/ui/accordion.tsx +51 -0
- package/src/admin/ui/components/ui/alert-dialog.tsx +98 -0
- package/src/admin/ui/components/ui/blocks.tsx +32 -0
- package/src/admin/ui/components/ui/breadcrumbs.tsx +59 -0
- package/src/admin/ui/components/ui/button.tsx +26 -0
- package/src/admin/ui/components/ui/collapsible.tsx +124 -0
- package/src/admin/ui/components/ui/dialog.tsx +79 -0
- package/src/admin/ui/components/ui/group.tsx +20 -0
- package/src/admin/ui/components/ui/index.ts +17 -0
- package/src/admin/ui/components/ui/input.tsx +12 -0
- package/src/admin/ui/components/ui/join.tsx +53 -0
- package/src/admin/ui/components/ui/label.tsx +11 -0
- package/src/admin/ui/components/ui/radio-group.tsx +75 -0
- package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +122 -0
- package/src/admin/ui/components/ui/relationship.tsx +58 -0
- package/src/admin/ui/components/ui/scroll-area.tsx +19 -0
- package/src/admin/ui/components/ui/select.tsx +187 -0
- package/src/admin/ui/components/ui/separator.tsx +21 -0
- package/src/admin/ui/components/ui/sheet.tsx +106 -0
- package/src/admin/ui/components/ui/tabs.tsx +116 -0
- package/src/admin/ui/components/ui/utils.ts +3 -0
- package/src/admin/ui/hooks/use-debounce.ts +15 -0
- package/src/admin/ui/styles/_locale-switcher.scss +33 -0
- package/src/admin/ui/styles/accordion.scss +60 -0
- package/src/admin/ui/styles/animations.scss +41 -0
- package/src/admin/ui/styles/asset-manager.scss +547 -0
- package/src/admin/ui/styles/badge.scss +13 -0
- package/src/admin/ui/styles/base.scss +22 -0
- package/src/admin/ui/styles/button.scss +161 -0
- package/src/admin/ui/styles/card.scss +13 -0
- package/src/admin/ui/styles/collapsible.scss +75 -0
- package/src/admin/ui/styles/data-detail.scss +92 -0
- package/src/admin/ui/styles/dialog.scss +102 -0
- package/src/admin/ui/styles/empty-state.scss +22 -0
- package/src/admin/ui/styles/group.scss +19 -0
- package/src/admin/ui/styles/index.scss +33 -0
- package/src/admin/ui/styles/input.scss +80 -0
- package/src/admin/ui/styles/label.scss +12 -0
- package/src/admin/ui/styles/layout.scss +56 -0
- package/src/admin/ui/styles/lexical.scss +469 -0
- package/src/admin/ui/styles/loading.scss +102 -0
- package/src/admin/ui/styles/media-registry.scss +597 -0
- package/src/admin/ui/styles/pagination.scss +20 -0
- package/src/admin/ui/styles/radio-group.scss +66 -0
- package/src/admin/ui/styles/row.scss +17 -0
- package/src/admin/ui/styles/scrollbar.scss +36 -0
- package/src/admin/ui/styles/select.scss +121 -0
- package/src/admin/ui/styles/separator.scss +14 -0
- package/src/admin/ui/styles/sheet.scss +152 -0
- package/src/admin/ui/styles/sidebar.scss +148 -0
- package/src/admin/ui/styles/switch.scss +59 -0
- package/src/admin/ui/styles/table.scss +207 -0
- package/src/admin/ui/styles/tabs.scss +62 -0
- package/src/admin/ui/styles/toast.scss +45 -0
- package/src/admin/ui/styles/variables.scss +24 -0
- package/src/admin/ui/views/collection-list-view.tsx +720 -0
- package/src/admin/ui/views/dashboard-view.tsx +263 -0
- package/src/admin/ui/views/document-edit-view.tsx +384 -0
- package/src/admin/ui/views/global-edit-view.tsx +226 -0
- package/src/admin/ui/views/init-view.tsx +182 -0
- package/src/admin/ui/views/login-view.tsx +123 -0
- package/src/admin/ui/views/media-registry-view.tsx +1104 -0
- package/src/admin/ui/views/settings-view.tsx +729 -0
- package/src/admin/webcomponent.tsx +15 -0
- package/src/api.ts +9 -0
- package/src/auth/index.ts +194 -0
- package/src/auth/migrations.ts +87 -0
- package/src/auth/premissions.ts +46 -0
- package/src/cli/commands/generate-types.ts +116 -0
- package/src/cli/commands/init.ts +95 -0
- package/src/cli/commands/migrate-commands.ts +160 -0
- package/src/cli/commands/seed-command.ts +11 -0
- package/src/cli/d1-mock.ts +101 -0
- package/src/cli/index.test.ts +84 -0
- package/src/cli/index.ts +183 -0
- package/src/cli/r2-mock.ts +217 -0
- package/src/cli/seeding.ts +405 -0
- package/src/client.ts +181 -0
- package/src/config-utils.ts +102 -0
- package/src/config.ts +49 -0
- package/src/db/adapter.ts +53 -0
- package/src/db/better-sqlite.ts +630 -0
- package/src/db/bun-sqlite.ts +646 -0
- package/src/db/d1.ts +711 -0
- package/src/db/kysely/data-mapper.ts +142 -0
- package/src/db/kysely/field-mapper.ts +148 -0
- package/src/db/kysely/migration-generator.ts +223 -0
- package/src/db/kysely/query-builder.ts +92 -0
- package/src/db/kysely/schema-builder.ts +439 -0
- package/src/db/kysely/sql-utils.ts +13 -0
- package/src/db/postgres.ts +621 -0
- package/src/db/sqlite.ts +658 -0
- package/src/db/system-schema.ts +121 -0
- package/src/index.ts +13 -0
- package/src/runtimes/README.md +59 -0
- package/src/runtimes/bun.ts +49 -0
- package/src/runtimes/cloudflare-workers.ts +38 -0
- package/src/runtimes/next.ts +26 -0
- package/src/runtimes/node.ts +52 -0
- package/src/schema/collection.ts +184 -0
- package/src/schema/fields/base.ts +164 -0
- package/src/schema/fields/index.ts +427 -0
- package/src/schema/global.ts +145 -0
- package/src/schema/index.ts +4 -0
- package/src/schema/infer.ts +72 -0
- package/src/server/admin-router.ts +20 -0
- package/src/server/admin.ts +142 -0
- package/src/server/assets.ts +306 -0
- package/src/server/collection-router.ts +55 -0
- package/src/server/handlers.ts +722 -0
- package/src/server/middlewares/admin.ts +27 -0
- package/src/server/middlewares/auth.ts +89 -0
- package/src/server/middlewares/context.ts +17 -0
- package/src/server/middlewares/cors.ts +24 -0
- package/src/server/middlewares/database-init.ts +74 -0
- package/src/server/middlewares/rate-limit.ts +71 -0
- package/src/server/router.ts +47 -0
- package/src/server/setup-middlewares.ts +58 -0
- package/src/server/system-router.ts +35 -0
- package/src/server.ts +9 -0
- package/src/storage/adapters/cloudflare-r2.ts +136 -0
- package/src/storage/adapters/local.ts +146 -0
- package/src/storage/adapters/s3.ts +186 -0
- package/src/storage/errors.ts +46 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/types.ts +39 -0
- package/src/types.ts +577 -0
- package/src/utils/lexical.ts +37 -0
- package/src/utils/logger.ts +73 -0
- package/src/validation.ts +429 -0
- package/src/validator.ts +179 -0
- package/test/admin-custom-field.test.ts +162 -0
- package/test/admin-react-field.test.tsx +134 -0
- package/test/api-features.test.ts +78 -0
- package/test/api.test.ts +178 -0
- package/test/auth.test.ts +62 -0
- package/test/cli-integration.test.ts +146 -0
- package/test/cli.test.ts +25 -0
- package/test/db/postgres.test.ts +95 -0
- package/test/db/sqlite-filter.test.ts +53 -0
- package/test/db/sqlite.test.ts +82 -0
- package/test/engine-features.test.ts +79 -0
- package/test/globals.test.ts +74 -0
- package/test/integration-tmp/db-app/opacacms.config.ts +15 -0
- package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +25 -0
- package/test/integration-tmp/my-test-app/index.ts +8 -0
- package/test/integration-tmp/my-test-app/opacacms.config.ts +16 -0
- package/test/integration-tmp/my-test-app/package.json +12 -0
- package/test/populate.test.ts +79 -0
- package/test/runtimes.test.ts +43 -0
- package/test/schema-builder.test.ts +107 -0
- package/test/schema-features.test.ts +63 -0
- package/test/seeding.test.ts +68 -0
- package/test/storage/local.test.ts +72 -0
- package/test/storage/s3.test.ts +60 -0
- package/test/structural-data.test.ts +100 -0
- package/test/test-setup.ts +11 -0
- package/test/validation.test.ts +162 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { GlobalWindow } from "happy-dom";
|
|
3
|
+
import { defineCustomField } from "../src/admin/custom-field";
|
|
4
|
+
|
|
5
|
+
describe("defineCustomField Adapter", () => {
|
|
6
|
+
let window: GlobalWindow;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
window = new GlobalWindow();
|
|
10
|
+
// Simulate browser environment globally for the tests
|
|
11
|
+
(globalThis as any).window = window;
|
|
12
|
+
(globalThis as any).document = window.document;
|
|
13
|
+
(globalThis as any).customElements = window.customElements;
|
|
14
|
+
(globalThis as any).HTMLElement = window.HTMLElement;
|
|
15
|
+
(globalThis as any).CustomEvent = window.CustomEvent;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
// Clean up globals
|
|
20
|
+
(globalThis as any).window = undefined;
|
|
21
|
+
(globalThis as any).document = undefined;
|
|
22
|
+
(globalThis as any).customElements = undefined;
|
|
23
|
+
(globalThis as any).HTMLElement = undefined;
|
|
24
|
+
(globalThis as any).CustomEvent = undefined;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should define a new custom element in the registry", () => {
|
|
28
|
+
defineCustomField("my-test-field", { mount: () => { } });
|
|
29
|
+
expect(customElements.get("my-test-field")).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should call the mount hook when inserted into the DOM", () => {
|
|
33
|
+
const mountMock = mock((container, props) => { });
|
|
34
|
+
defineCustomField("my-mounted-field", { mount: mountMock });
|
|
35
|
+
|
|
36
|
+
const el = document.createElement("my-mounted-field");
|
|
37
|
+
document.body.appendChild(el);
|
|
38
|
+
|
|
39
|
+
expect(mountMock).toHaveBeenCalledTimes(1);
|
|
40
|
+
expect(mountMock.mock.calls[0]![0]).toBe(el); // Passes container
|
|
41
|
+
|
|
42
|
+
// Checks props object structure
|
|
43
|
+
const props = mountMock.mock.calls[0]![1];
|
|
44
|
+
expect(props).toHaveProperty("value");
|
|
45
|
+
expect(props).toHaveProperty("fieldConfig");
|
|
46
|
+
expect(props).toHaveProperty("disabled", false);
|
|
47
|
+
expect(props).toHaveProperty("readOnly", false);
|
|
48
|
+
expect(typeof props.onChange).toBe("function");
|
|
49
|
+
|
|
50
|
+
document.body.removeChild(el);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should parse boolean attributes correctly on mount", () => {
|
|
54
|
+
const mountMock = mock((container, props) => { });
|
|
55
|
+
defineCustomField("my-boolean-field", { mount: mountMock });
|
|
56
|
+
|
|
57
|
+
const el = document.createElement("my-boolean-field");
|
|
58
|
+
el.setAttribute("data-disabled", "true");
|
|
59
|
+
el.setAttribute("data-readonly", "true");
|
|
60
|
+
document.body.appendChild(el);
|
|
61
|
+
|
|
62
|
+
const props = mountMock.mock.calls[0]![1];
|
|
63
|
+
expect(props.disabled).toBe(true);
|
|
64
|
+
expect(props.readOnly).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should call update hook when value property changes", () => {
|
|
68
|
+
const updateMock = mock((container, props) => { });
|
|
69
|
+
defineCustomField("my-update-field", {
|
|
70
|
+
mount: () => { },
|
|
71
|
+
update: updateMock,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const el = document.createElement("my-update-field");
|
|
75
|
+
document.body.appendChild(el);
|
|
76
|
+
|
|
77
|
+
// Initial value change
|
|
78
|
+
(el as any).value = "new value";
|
|
79
|
+
|
|
80
|
+
expect(updateMock).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(updateMock.mock.calls[0]![1].value).toBe("new value");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should not call update hook if value property is identical", () => {
|
|
85
|
+
const updateMock = mock((container, props) => { });
|
|
86
|
+
defineCustomField("my-update-cache-field", {
|
|
87
|
+
mount: () => { },
|
|
88
|
+
update: updateMock,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const el = document.createElement("my-update-cache-field");
|
|
92
|
+
(el as any).value = "initial"; // Set before mount
|
|
93
|
+
document.body.appendChild(el);
|
|
94
|
+
|
|
95
|
+
// Identical value change
|
|
96
|
+
(el as any).value = "initial";
|
|
97
|
+
expect(updateMock).toHaveBeenCalledTimes(0);
|
|
98
|
+
|
|
99
|
+
// Real change
|
|
100
|
+
(el as any).value = "new value";
|
|
101
|
+
expect(updateMock).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should call update hook when fieldConfig property changes", () => {
|
|
105
|
+
const updateMock = mock((container, props) => { });
|
|
106
|
+
defineCustomField("my-config-field", {
|
|
107
|
+
mount: () => { },
|
|
108
|
+
update: updateMock,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const el = document.createElement("my-config-field");
|
|
112
|
+
document.body.appendChild(el);
|
|
113
|
+
|
|
114
|
+
(el as any).fieldConfig = { slug: "test" };
|
|
115
|
+
|
|
116
|
+
expect(updateMock).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(updateMock.mock.calls[0]![1].fieldConfig).toEqual({ slug: "test" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should dispatch opacachange event when onChange is called", () => {
|
|
121
|
+
let internalOnChange: (v: any) => void = () => { };
|
|
122
|
+
|
|
123
|
+
defineCustomField("my-event-field", {
|
|
124
|
+
mount: (container, props) => {
|
|
125
|
+
internalOnChange = props.onChange;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const el = document.createElement("my-event-field");
|
|
130
|
+
document.body.appendChild(el);
|
|
131
|
+
|
|
132
|
+
const eventListener = mock((e: CustomEvent) => {
|
|
133
|
+
expect(e.detail.value).toBe("hello world");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Listen to the document, because bubbles and composed should be true
|
|
137
|
+
document.addEventListener("opacachange", eventListener as unknown as EventListener);
|
|
138
|
+
|
|
139
|
+
// Trigger the abstraction
|
|
140
|
+
internalOnChange("hello world");
|
|
141
|
+
|
|
142
|
+
expect(eventListener).toHaveBeenCalledTimes(1);
|
|
143
|
+
|
|
144
|
+
document.removeEventListener("opacachange", eventListener as unknown as EventListener);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should call the unmount hook when removed from the DOM", () => {
|
|
148
|
+
const unmountMock = mock((container) => { });
|
|
149
|
+
defineCustomField("my-unmount-field", {
|
|
150
|
+
mount: () => { },
|
|
151
|
+
unmount: unmountMock,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const el = document.createElement("my-unmount-field");
|
|
155
|
+
document.body.appendChild(el);
|
|
156
|
+
expect(unmountMock).toHaveBeenCalledTimes(0);
|
|
157
|
+
|
|
158
|
+
// Remove from DOM
|
|
159
|
+
document.body.removeChild(el);
|
|
160
|
+
expect(unmountMock).toHaveBeenCalledTimes(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
+
import { GlobalWindow } from 'happy-dom';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { defineReactField } from '../src/admin/react';
|
|
5
|
+
|
|
6
|
+
describe('defineReactField React Adapter', () => {
|
|
7
|
+
let window: GlobalWindow;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
window = new GlobalWindow();
|
|
11
|
+
// Simulate browser environment globally for the tests
|
|
12
|
+
(globalThis as any).window = window;
|
|
13
|
+
(globalThis as any).document = window.document;
|
|
14
|
+
(globalThis as any).customElements = window.customElements;
|
|
15
|
+
(globalThis as any).HTMLElement = window.HTMLElement;
|
|
16
|
+
(globalThis as any).CustomEvent = window.CustomEvent;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// Clean up globals
|
|
21
|
+
(globalThis as any).window = undefined;
|
|
22
|
+
(globalThis as any).document = undefined;
|
|
23
|
+
(globalThis as any).customElements = undefined;
|
|
24
|
+
(globalThis as any).HTMLElement = undefined;
|
|
25
|
+
(globalThis as any).CustomEvent = undefined;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should define a React custom field successfully', async () => {
|
|
29
|
+
const renderSpy = mock((props) => {
|
|
30
|
+
return React.createElement(
|
|
31
|
+
'div',
|
|
32
|
+
{ id: 'test-react-field' },
|
|
33
|
+
props.value,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
defineReactField('my-test-react', renderSpy);
|
|
38
|
+
|
|
39
|
+
const el = document.createElement('my-test-react');
|
|
40
|
+
(el as any).value = 'Hello React';
|
|
41
|
+
document.body.appendChild(el);
|
|
42
|
+
|
|
43
|
+
// Wait for React to asynchronously flush renders to DOM
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
45
|
+
|
|
46
|
+
expect(renderSpy).toHaveBeenCalled();
|
|
47
|
+
expect(renderSpy.mock.calls[0]![0].value).toBe('Hello React');
|
|
48
|
+
|
|
49
|
+
// Check if React actually created the child node
|
|
50
|
+
const child = el.querySelector('#test-react-field');
|
|
51
|
+
expect(child).not.toBeNull();
|
|
52
|
+
expect(child?.textContent).toBe('Hello React');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should re-render React component when custom element props change', async () => {
|
|
56
|
+
const renderSpy = mock((props) => {
|
|
57
|
+
return React.createElement('div', null, `error: ${props.error}`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
defineReactField('my-error-react', renderSpy);
|
|
61
|
+
|
|
62
|
+
const el = document.createElement('my-error-react');
|
|
63
|
+
document.body.appendChild(el);
|
|
64
|
+
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
66
|
+
|
|
67
|
+
// Update error prop externally, mimicking CMS behavior
|
|
68
|
+
(el as any).error = 'Invalid slug format';
|
|
69
|
+
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
71
|
+
|
|
72
|
+
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
73
|
+
expect(renderSpy.mock.calls[1]![0].error).toBe('Invalid slug format');
|
|
74
|
+
expect(el.textContent).toBe('error: Invalid slug format');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should correctly receive parentData prop', async () => {
|
|
78
|
+
const renderSpy = mock((props) => {
|
|
79
|
+
return React.createElement(
|
|
80
|
+
'div',
|
|
81
|
+
null,
|
|
82
|
+
props.parentData?.country || 'none',
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
defineReactField('my-parent-react', renderSpy);
|
|
87
|
+
|
|
88
|
+
const el = document.createElement('my-parent-react');
|
|
89
|
+
(el as any).parentData = { country: 'Brazil', taxCode: '123' };
|
|
90
|
+
document.body.appendChild(el);
|
|
91
|
+
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
93
|
+
|
|
94
|
+
expect(renderSpy.mock.calls[0]![0].parentData).toEqual({
|
|
95
|
+
country: 'Brazil',
|
|
96
|
+
taxCode: '123',
|
|
97
|
+
});
|
|
98
|
+
expect(el.textContent).toBe('Brazil');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should unmount React root to prevent memory leaks when element is removed', async () => {
|
|
102
|
+
let effectRan = false;
|
|
103
|
+
let effectCleanedUp = false;
|
|
104
|
+
|
|
105
|
+
// A real React component utilizing Hooks
|
|
106
|
+
const MyComponent = () => {
|
|
107
|
+
React.useEffect(() => {
|
|
108
|
+
effectRan = true;
|
|
109
|
+
return () => {
|
|
110
|
+
effectCleanedUp = true;
|
|
111
|
+
};
|
|
112
|
+
}, []);
|
|
113
|
+
return React.createElement('span', null, 'Mounting');
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
defineReactField('my-unmount-react', MyComponent);
|
|
117
|
+
|
|
118
|
+
const el = document.createElement('my-unmount-react');
|
|
119
|
+
document.body.appendChild(el);
|
|
120
|
+
|
|
121
|
+
// Wait for React render + useEffect
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
123
|
+
expect(effectRan).toBe(true);
|
|
124
|
+
|
|
125
|
+
// Remove the custom element
|
|
126
|
+
document.body.removeChild(el);
|
|
127
|
+
|
|
128
|
+
// Wait for the asynchronous unmount timeout (from our helper implementation)
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
130
|
+
|
|
131
|
+
expect(effectCleanedUp).toBe(true);
|
|
132
|
+
expect((el as any)._opacaReactRoot).toBeUndefined(); // Check root reference deleted
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { createBunSQLiteAdapter } from "../src/db/bun-sqlite";
|
|
4
|
+
import { Collection, Field } from "../src/schema";
|
|
5
|
+
import { createAPIRouter } from "../src/server/router";
|
|
6
|
+
import type { OpacaConfig } from "../src/types";
|
|
7
|
+
|
|
8
|
+
describe("API Schema Features Integration", () => {
|
|
9
|
+
const dbPath = ":memory:";
|
|
10
|
+
const db = createBunSQLiteAdapter(dbPath);
|
|
11
|
+
|
|
12
|
+
const postsCollection = Collection.create("posts")
|
|
13
|
+
.fields([Field.text("title").required(), Field.slug("slug").from("title")])
|
|
14
|
+
.webhooks([
|
|
15
|
+
{
|
|
16
|
+
events: ["afterCreate"],
|
|
17
|
+
url: "https://example.com/api/webhook",
|
|
18
|
+
},
|
|
19
|
+
])
|
|
20
|
+
.build();
|
|
21
|
+
|
|
22
|
+
const config = {
|
|
23
|
+
db,
|
|
24
|
+
collections: [postsCollection],
|
|
25
|
+
} as unknown as OpacaConfig;
|
|
26
|
+
|
|
27
|
+
const apiRouter = createAPIRouter(config);
|
|
28
|
+
const app = new Hono();
|
|
29
|
+
app.route("/", apiRouter);
|
|
30
|
+
|
|
31
|
+
test("Should auto-generate slug on POST if not provided", async () => {
|
|
32
|
+
// Wait for the migration logic or use db.migrate directly.
|
|
33
|
+
await db.migrate([postsCollection]);
|
|
34
|
+
|
|
35
|
+
const req = new Request("http://localhost/api/posts", {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
title: "Hello World 2024!",
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const res = await app.fetch(req);
|
|
44
|
+
expect(res.status).toBe(201);
|
|
45
|
+
|
|
46
|
+
const json = (await res.json()) as any;
|
|
47
|
+
expect(json.title).toBe("Hello World 2024!");
|
|
48
|
+
expect(json.slug).toBe("hello-world-2024");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("Should call webhook on afterCreate event", async () => {
|
|
52
|
+
const globalFetch = globalThis.fetch;
|
|
53
|
+
const fetchMock = mock(async () => new Response());
|
|
54
|
+
globalThis.fetch = fetchMock as any;
|
|
55
|
+
|
|
56
|
+
const req = new Request("http://localhost/api/posts", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
title: "Webhook Test",
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const res = await app.fetch(req);
|
|
65
|
+
expect(res.status).toBe(201);
|
|
66
|
+
|
|
67
|
+
// Wait a brief moment for the asynchronous fetch to execute
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
69
|
+
|
|
70
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
71
|
+
const mockCallArgs = fetchMock.mock.calls[0] as any;
|
|
72
|
+
expect(mockCallArgs[0]).toBe("https://example.com/api/webhook");
|
|
73
|
+
expect(mockCallArgs[1].method).toBe("POST");
|
|
74
|
+
expect(JSON.parse(mockCallArgs[1].body).title).toBe("Webhook Test");
|
|
75
|
+
|
|
76
|
+
globalThis.fetch = globalFetch;
|
|
77
|
+
});
|
|
78
|
+
});
|
package/test/api.test.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { BunSQLiteAdapter } from "../src/db/bun-sqlite";
|
|
3
|
+
import { createAPIRouter } from "../src/server/router";
|
|
4
|
+
import type { Collection, OpacaConfig } from "../src/types";
|
|
5
|
+
|
|
6
|
+
describe("API Router", () => {
|
|
7
|
+
let db: BunSQLiteAdapter;
|
|
8
|
+
let app: any;
|
|
9
|
+
|
|
10
|
+
const testCollection: Collection = {
|
|
11
|
+
slug: "posts",
|
|
12
|
+
fields: [
|
|
13
|
+
{ name: "title", type: "text" },
|
|
14
|
+
{ name: "views", type: "number" },
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const config: OpacaConfig = {
|
|
19
|
+
db: null as any, // Will set later
|
|
20
|
+
collections: [testCollection],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
db = new BunSQLiteAdapter(":memory:");
|
|
25
|
+
await db.migrate([testCollection]);
|
|
26
|
+
config.db = db;
|
|
27
|
+
process.env.BETTER_AUTH_URL = "http://localhost:3000";
|
|
28
|
+
process.env.BETTER_AUTH_SECRET = "secret_for_test";
|
|
29
|
+
app = createAPIRouter(config);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("GET /:slug should return empty list initially", async () => {
|
|
33
|
+
const res = await app.request("/api/posts");
|
|
34
|
+
expect(res.status).toBe(200);
|
|
35
|
+
expect(res.headers.get("X-Powered-By")).toBe("OpacaCMS");
|
|
36
|
+
const body = await res.json();
|
|
37
|
+
expect(body.docs).toEqual([]);
|
|
38
|
+
expect(body.totalDocs).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("POST /:slug should create a document", async () => {
|
|
42
|
+
const res = await app.request("/api/posts", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body: JSON.stringify({ title: "New API Post", views: 100 }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(res.status).toBe(201);
|
|
49
|
+
const doc = await res.json();
|
|
50
|
+
expect(doc.title).toBe("New API Post");
|
|
51
|
+
expect(doc.id).toBeDefined();
|
|
52
|
+
|
|
53
|
+
// Verify persistence
|
|
54
|
+
const saved = await db.findOne("posts", { id: (doc as any).id });
|
|
55
|
+
expect(saved).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("GET /:slug/:id should return a document", async () => {
|
|
59
|
+
const doc = await db.create("posts", { title: "Find Me" });
|
|
60
|
+
|
|
61
|
+
const res = await app.request(`/api/posts/${(doc as any).id}`);
|
|
62
|
+
expect(res.status).toBe(200);
|
|
63
|
+
const body = await res.json();
|
|
64
|
+
expect(body.title).toBe("Find Me");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("PATCH /:slug/:id should update a document", async () => {
|
|
68
|
+
const doc = await db.create("posts", { title: "Update Me" });
|
|
69
|
+
|
|
70
|
+
const res = await app.request(`/api/posts/${(doc as any).id}`, {
|
|
71
|
+
method: "PATCH",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ title: "Updated" }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(res.status).toBe(200);
|
|
77
|
+
const body = (await res.json()) as any;
|
|
78
|
+
expect(body.title).toBe("Updated");
|
|
79
|
+
|
|
80
|
+
const check = await db.findOne("posts", { id: (doc as any).id });
|
|
81
|
+
expect((check as any)?.title).toBe("Updated");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("DELETE /:slug/:id should delete a document", async () => {
|
|
85
|
+
const doc = await db.create("posts", { title: "Delete Me" });
|
|
86
|
+
|
|
87
|
+
const res = await app.request(`/api/posts/${(doc as any).id}`, {
|
|
88
|
+
method: "DELETE",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(res.status).toBe(200);
|
|
92
|
+
|
|
93
|
+
const check = await db.findOne("posts", { id: (doc as any).id });
|
|
94
|
+
expect(check).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("POST /:slug should fail validation if required field missing", async () => {
|
|
98
|
+
const requiredCollection: Collection = {
|
|
99
|
+
slug: "reqposts",
|
|
100
|
+
fields: [{ name: "title", type: "text", required: true }],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await db.migrate([requiredCollection]);
|
|
104
|
+
const newConfig = { ...config, collections: [...config.collections, requiredCollection] };
|
|
105
|
+
const newApp = createAPIRouter(newConfig);
|
|
106
|
+
|
|
107
|
+
const res = await newApp.request("/api/reqposts", {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({}), // Missing title
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(res.status).toBe(400);
|
|
114
|
+
const body = (await res.json()) as any;
|
|
115
|
+
expect(body.errors).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("POST /:slug should fail validation if wrong type", async () => {
|
|
119
|
+
const res = await app.request("/api/posts", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "Content-Type": "application/json" },
|
|
122
|
+
body: JSON.stringify({ title: 123 }), // title is text, expected string
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(res.status).toBe(400);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("GET /:slug should support pagination", async () => {
|
|
129
|
+
// Create 15 items
|
|
130
|
+
for (let i = 1; i <= 15; i++) {
|
|
131
|
+
await db.create("posts", { title: `Post ${i}`, views: i });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Page 1
|
|
135
|
+
const res1 = await app.request("/api/posts?limit=10&page=1");
|
|
136
|
+
const body1 = await res1.json();
|
|
137
|
+
expect(body1.docs.length).toBe(10);
|
|
138
|
+
expect(body1.totalDocs).toBe(15);
|
|
139
|
+
expect(body1.totalPages).toBe(2);
|
|
140
|
+
expect(body1.page).toBe(1);
|
|
141
|
+
expect(body1.hasNextPage).toBe(true);
|
|
142
|
+
|
|
143
|
+
// Page 2
|
|
144
|
+
const res2 = await app.request("/api/posts?limit=10&page=2");
|
|
145
|
+
const body2 = await res2.json();
|
|
146
|
+
expect(body2.docs.length).toBe(5);
|
|
147
|
+
expect(body2.page).toBe(2);
|
|
148
|
+
expect(body2.hasNextPage).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("GET /:slug should support sorting", async () => {
|
|
152
|
+
await db.create("posts", { title: "A", views: 10 });
|
|
153
|
+
await db.create("posts", { title: "B", views: 20 });
|
|
154
|
+
|
|
155
|
+
// Descending
|
|
156
|
+
const resDesc = await app.request("/api/posts?sort=-views");
|
|
157
|
+
const bodyDesc = await resDesc.json();
|
|
158
|
+
expect(bodyDesc.docs[0].views).toBe(20);
|
|
159
|
+
|
|
160
|
+
// Ascending
|
|
161
|
+
const resAsc = await app.request("/api/posts?sort=views");
|
|
162
|
+
const bodyAsc = await resAsc.json();
|
|
163
|
+
expect(bodyAsc.docs[0].views).toBe(10);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("GET /:slug should support advanced filters", async () => {
|
|
167
|
+
await db.create("posts", { title: "Low", views: 5 });
|
|
168
|
+
await db.create("posts", { title: "Medium", views: 15 });
|
|
169
|
+
await db.create("posts", { title: "High", views: 25 });
|
|
170
|
+
|
|
171
|
+
// views > 10
|
|
172
|
+
// URL encoded: views[gt]=10
|
|
173
|
+
const res = await app.request("/api/posts?views[gt]=10");
|
|
174
|
+
const body = await res.json();
|
|
175
|
+
expect(body.docs.length).toBe(2);
|
|
176
|
+
expect(body.docs.find((d: any) => d.views === 5)).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { createBunSQLiteAdapter } from "../src/db/bun-sqlite";
|
|
4
|
+
import { createAPIRouter } from "../src/server/router";
|
|
5
|
+
import type { OpacaConfig } from "../src/types";
|
|
6
|
+
|
|
7
|
+
// Set Env for better-auth defaults
|
|
8
|
+
process.env.BETTER_AUTH_URL = "http://localhost/api/auth";
|
|
9
|
+
process.env.BETTER_AUTH_SECRET = "test-secret-12345678901234567890"; // Must be long enough
|
|
10
|
+
|
|
11
|
+
describe("Better Auth Integration", () => {
|
|
12
|
+
const dbPath = ":memory:";
|
|
13
|
+
const db = createBunSQLiteAdapter(dbPath);
|
|
14
|
+
|
|
15
|
+
const config = {
|
|
16
|
+
db,
|
|
17
|
+
collections: [{ slug: "posts", fields: [] }],
|
|
18
|
+
access: {
|
|
19
|
+
roles: {
|
|
20
|
+
editor: {
|
|
21
|
+
posts: ["create", "read"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
trustedOrigins: ["http://test.local"],
|
|
26
|
+
auth: {
|
|
27
|
+
// Empty auth object will use all defaults
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// We need to wait for migrations?
|
|
32
|
+
// Since runAuthMigrations is fire-and-forget in createAPIRouter, we might need a small delay or manual trigger for strict testing.
|
|
33
|
+
// But for route existence check, it shouldn't matter.
|
|
34
|
+
|
|
35
|
+
const apiRouter = createAPIRouter(config as any);
|
|
36
|
+
const app = new Hono();
|
|
37
|
+
app.route("/", apiRouter);
|
|
38
|
+
|
|
39
|
+
// TODO: Investigate why this returns 404 in bun test environment despite handler being hit.
|
|
40
|
+
// Admin protection check confirms auth.api is working.
|
|
41
|
+
test.skip("Auth routes should be mounted", async () => {
|
|
42
|
+
const req = new Request("http://localhost/api/auth/session");
|
|
43
|
+
const res = await app.fetch(req);
|
|
44
|
+
|
|
45
|
+
// Even if session is null, it should return 200 OK with null body or similar,
|
|
46
|
+
// OR 404 if route not found.
|
|
47
|
+
expect(res.status).not.toBe(404);
|
|
48
|
+
|
|
49
|
+
// better-auth /session usually returns 200 with null if no session
|
|
50
|
+
// or 401 depending on config, but definitely not 404.
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("Admin routes should be protected", async () => {
|
|
54
|
+
const req = new Request("http://localhost/api/__admin/collections");
|
|
55
|
+
const res = await app.fetch(req);
|
|
56
|
+
|
|
57
|
+
// Should be 401 Unauthorized
|
|
58
|
+
expect(res.status).toBe(401);
|
|
59
|
+
const body = await res.json();
|
|
60
|
+
expect(body).toEqual({ message: "Unauthorized" });
|
|
61
|
+
});
|
|
62
|
+
});
|