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,217 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A minimal R2Bucket mock for OpacaCMS CLI that uses Wrangler's local state.
|
|
8
|
+
* Stores metadata in _mf_objects table and blobs in .blobs sibling directory.
|
|
9
|
+
*/
|
|
10
|
+
export function createR2Mock(dbPath?: string) {
|
|
11
|
+
let sqlite: Database;
|
|
12
|
+
let blobsDir: string;
|
|
13
|
+
let finalDbPath: string | undefined;
|
|
14
|
+
|
|
15
|
+
// 1. Try to find Wrangler's local R2 state if no path provided
|
|
16
|
+
const wranglerR2Dir = path.resolve(
|
|
17
|
+
process.cwd(),
|
|
18
|
+
".wrangler/state/v3/r2/miniflare-R2BucketObject",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!dbPath && fs.existsSync(wranglerR2Dir)) {
|
|
22
|
+
const files = fs.readdirSync(wranglerR2Dir);
|
|
23
|
+
const sqliteFile = files.find((f) => f.endsWith(".sqlite"));
|
|
24
|
+
if (sqliteFile) {
|
|
25
|
+
finalDbPath = path.join(wranglerR2Dir, sqliteFile);
|
|
26
|
+
console.log(`[OpacaCMS] Using Wrangler R2 local state: ${sqliteFile}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Handle specific path or fallback
|
|
31
|
+
if (!finalDbPath) {
|
|
32
|
+
const inputPath = dbPath || ".opaca/local-r2/mock-bucket.sqlite";
|
|
33
|
+
const absolutePath = path.isAbsolute(inputPath)
|
|
34
|
+
? inputPath
|
|
35
|
+
: path.resolve(process.cwd(), inputPath);
|
|
36
|
+
|
|
37
|
+
// If it's a directory, append default filename
|
|
38
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
|
|
39
|
+
finalDbPath = path.join(absolutePath, "mock-bucket.sqlite");
|
|
40
|
+
} else if (!absolutePath.endsWith(".sqlite")) {
|
|
41
|
+
finalDbPath = absolutePath.endsWith("/")
|
|
42
|
+
? path.join(absolutePath, "mock-bucket.sqlite")
|
|
43
|
+
: absolutePath + ".sqlite";
|
|
44
|
+
} else {
|
|
45
|
+
finalDbPath = absolutePath;
|
|
46
|
+
}
|
|
47
|
+
console.log(`[OpacaCMS] Using local R2 mock: ${path.basename(finalDbPath)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ensure directory exists
|
|
51
|
+
const dir = path.dirname(finalDbPath);
|
|
52
|
+
if (!fs.existsSync(dir)) {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
sqlite = new Database(finalDbPath);
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
throw new Error(`Failed to open R2 mock database at ${finalDbPath}: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
blobsDir = finalDbPath.replace(".sqlite", ".blobs");
|
|
63
|
+
if (!fs.existsSync(blobsDir)) {
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
fs.mkdirSync(blobsDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Ensure table exists (miniflare 3 schema)
|
|
69
|
+
sqlite.exec(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS _mf_objects (
|
|
71
|
+
key TEXT PRIMARY KEY,
|
|
72
|
+
blob_id TEXT,
|
|
73
|
+
version TEXT,
|
|
74
|
+
size INTEGER,
|
|
75
|
+
etag TEXT,
|
|
76
|
+
uploaded INTEGER,
|
|
77
|
+
checksums TEXT,
|
|
78
|
+
http_metadata TEXT,
|
|
79
|
+
custom_metadata TEXT
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
const getBlobPath = (blobId: string) => path.join(blobsDir, blobId);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
async put(key: string, value: any, options?: any) {
|
|
87
|
+
const blobId = crypto.randomUUID();
|
|
88
|
+
const filePath = getBlobPath(blobId);
|
|
89
|
+
|
|
90
|
+
const buffer =
|
|
91
|
+
value instanceof Uint8Array
|
|
92
|
+
? value
|
|
93
|
+
: value instanceof ArrayBuffer
|
|
94
|
+
? new Uint8Array(value)
|
|
95
|
+
: typeof value === "string" || value instanceof Buffer
|
|
96
|
+
? Buffer.from(value)
|
|
97
|
+
: new Uint8Array(await (value as any).arrayBuffer());
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(filePath, buffer);
|
|
100
|
+
|
|
101
|
+
// MD5 for ETag (standard for R2/Miniflare)
|
|
102
|
+
const etag = crypto.createHash("md5").update(buffer).digest("hex");
|
|
103
|
+
|
|
104
|
+
// Miniflare 3 internal SQLite state often use MILLIS for 'uploaded'
|
|
105
|
+
// despite the R2 API returning seconds. Let's use MILLIS.
|
|
106
|
+
const uploaded = Date.now();
|
|
107
|
+
const size = buffer.length;
|
|
108
|
+
|
|
109
|
+
// Ensure we use the exact keys expected by Workerd/Miniflare
|
|
110
|
+
const httpMetadataObj: Record<string, string> = {};
|
|
111
|
+
if (options?.httpMetadata?.contentType) {
|
|
112
|
+
httpMetadataObj.contentType = options.httpMetadata.contentType;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const customMetadataObj: Record<string, string> = { ...options?.customMetadata };
|
|
116
|
+
if (options?.customMetadata?.sourceUrl) {
|
|
117
|
+
customMetadataObj.sourceUrl = options.customMetadata.sourceUrl;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const httpMetadata = JSON.stringify(httpMetadataObj);
|
|
121
|
+
const customMetadata = JSON.stringify(customMetadataObj);
|
|
122
|
+
|
|
123
|
+
sqlite
|
|
124
|
+
.prepare(`
|
|
125
|
+
INSERT OR REPLACE INTO _mf_objects
|
|
126
|
+
(key, blob_id, version, size, etag, uploaded, checksums, http_metadata, custom_metadata)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
128
|
+
`)
|
|
129
|
+
.run(key, blobId, "v1", size, etag, uploaded, "{}", httpMetadata, customMetadata);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
key,
|
|
133
|
+
size,
|
|
134
|
+
etag,
|
|
135
|
+
httpMetadata: options?.httpMetadata || {},
|
|
136
|
+
customMetadata: options?.customMetadata || {},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async get(key: string) {
|
|
141
|
+
const row = sqlite.prepare("SELECT * FROM _mf_objects WHERE key = ?").get(key) as any;
|
|
142
|
+
if (!row) return null;
|
|
143
|
+
|
|
144
|
+
const filePath = getBlobPath(row.blob_id);
|
|
145
|
+
if (!fs.existsSync(filePath)) return null;
|
|
146
|
+
|
|
147
|
+
const buffer = fs.readFileSync(filePath);
|
|
148
|
+
const stream = new ReadableStream({
|
|
149
|
+
start(controller) {
|
|
150
|
+
controller.enqueue(new Uint8Array(buffer));
|
|
151
|
+
controller.close();
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
key: row.key,
|
|
157
|
+
size: row.size,
|
|
158
|
+
etag: row.etag,
|
|
159
|
+
uploaded: new Date(row.uploaded), // Works with both s and ms
|
|
160
|
+
httpMetadata: JSON.parse(row.http_metadata || "{}"),
|
|
161
|
+
customMetadata: JSON.parse(row.custom_metadata || "{}"),
|
|
162
|
+
body: stream,
|
|
163
|
+
bodyUsed: false,
|
|
164
|
+
async arrayBuffer() {
|
|
165
|
+
return buffer.buffer;
|
|
166
|
+
},
|
|
167
|
+
async text() {
|
|
168
|
+
return buffer.toString();
|
|
169
|
+
},
|
|
170
|
+
async json() {
|
|
171
|
+
return JSON.parse(buffer.toString());
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async head(key: string) {
|
|
177
|
+
const row = sqlite
|
|
178
|
+
.prepare(
|
|
179
|
+
"SELECT key, size, etag, http_metadata, custom_metadata, uploaded FROM _mf_objects WHERE key = ?",
|
|
180
|
+
)
|
|
181
|
+
.get(key) as any;
|
|
182
|
+
if (!row) return null;
|
|
183
|
+
return {
|
|
184
|
+
key: row.key,
|
|
185
|
+
size: row.size,
|
|
186
|
+
etag: row.etag,
|
|
187
|
+
uploaded: new Date(row.uploaded),
|
|
188
|
+
httpMetadata: JSON.parse(row.http_metadata || "{}"),
|
|
189
|
+
customMetadata: JSON.parse(row.custom_metadata || "{}"),
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async delete(key: string) {
|
|
194
|
+
const row = sqlite.prepare("SELECT blob_id FROM _mf_objects WHERE key = ?").get(key) as any;
|
|
195
|
+
if (row) {
|
|
196
|
+
const filePath = getBlobPath(row.blob_id);
|
|
197
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
198
|
+
sqlite.prepare("DELETE FROM _mf_objects WHERE key = ?").run(key);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
async list() {
|
|
203
|
+
const rows = sqlite.prepare("SELECT * FROM _mf_objects").all() as any[];
|
|
204
|
+
return {
|
|
205
|
+
objects: rows.map((row) => ({
|
|
206
|
+
key: row.key,
|
|
207
|
+
size: row.size,
|
|
208
|
+
etag: row.etag,
|
|
209
|
+
uploaded: new Date(row.uploaded),
|
|
210
|
+
httpMetadata: JSON.parse(row.http_metadata || "{}"),
|
|
211
|
+
customMetadata: JSON.parse(row.custom_metadata || "{}"),
|
|
212
|
+
})),
|
|
213
|
+
truncated: false,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { faker } from "@faker-js/faker";
|
|
2
|
+
import type { Collection, DatabaseAdapter, Field, OpacaConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default generators for each field type.
|
|
6
|
+
*/
|
|
7
|
+
export const defaultFieldGenerators: Record<string, () => any> = {
|
|
8
|
+
text: () => faker.lorem.words(3),
|
|
9
|
+
textarea: () => faker.lorem.paragraph(),
|
|
10
|
+
number: () => faker.number.int({ min: 1, max: 1000 }),
|
|
11
|
+
richtext: () =>
|
|
12
|
+
JSON.stringify({
|
|
13
|
+
root: {
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
detail: 0,
|
|
19
|
+
format: 0,
|
|
20
|
+
mode: "normal",
|
|
21
|
+
style: "",
|
|
22
|
+
text: faker.lorem.sentence(),
|
|
23
|
+
type: "text",
|
|
24
|
+
version: 1,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
direction: "ltr",
|
|
28
|
+
format: "",
|
|
29
|
+
indent: 0,
|
|
30
|
+
type: "heading",
|
|
31
|
+
tag: "h1",
|
|
32
|
+
version: 1,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
children: [
|
|
36
|
+
{
|
|
37
|
+
detail: 0,
|
|
38
|
+
format: 0,
|
|
39
|
+
mode: "normal",
|
|
40
|
+
style: "",
|
|
41
|
+
text: faker.lorem.paragraphs(2),
|
|
42
|
+
type: "text",
|
|
43
|
+
version: 1,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
direction: "ltr",
|
|
47
|
+
format: "",
|
|
48
|
+
indent: 0,
|
|
49
|
+
type: "paragraph",
|
|
50
|
+
version: 1,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
direction: "ltr",
|
|
54
|
+
format: "",
|
|
55
|
+
indent: 0,
|
|
56
|
+
type: "root",
|
|
57
|
+
version: 1,
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
boolean: () => faker.datatype.boolean(),
|
|
61
|
+
date: () => faker.date.recent().toISOString(),
|
|
62
|
+
email: () => faker.internet.email(),
|
|
63
|
+
json: () => ({ [faker.lorem.word()]: faker.lorem.sentence() }),
|
|
64
|
+
select: () => faker.lorem.word(),
|
|
65
|
+
radio: () => faker.lorem.word(),
|
|
66
|
+
// Add block generator template
|
|
67
|
+
blocks: () => [],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
async function getRandomIds(
|
|
71
|
+
db: DatabaseAdapter,
|
|
72
|
+
relationTo: string,
|
|
73
|
+
count: number = 1,
|
|
74
|
+
): Promise<any[]> {
|
|
75
|
+
try {
|
|
76
|
+
const result = await db.find(relationTo, {}, { limit: 50 });
|
|
77
|
+
const docs = result.docs || [];
|
|
78
|
+
|
|
79
|
+
if (docs.length === 0) {
|
|
80
|
+
// Don't log for system collections or if it's expected
|
|
81
|
+
if (!relationTo.startsWith("_")) {
|
|
82
|
+
console.warn(`[Seeding] No documents found in '${relationTo}' for relationship.`);
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Shuffle and pick
|
|
88
|
+
const shuffled = [...docs].sort(() => 0.5 - Math.random());
|
|
89
|
+
const selected = shuffled.slice(0, count).map((doc) => (doc as any).id);
|
|
90
|
+
return selected;
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(`[Seeding] Failed to fetch random IDs for ${relationTo}:`, e);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolves a relationship by picking a random ID from the target collection.
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generates data for a list of fields, recursively handling groups and layouts.
|
|
103
|
+
*/
|
|
104
|
+
async function generateDataForFields(
|
|
105
|
+
fields: Field[],
|
|
106
|
+
db: DatabaseAdapter,
|
|
107
|
+
locales: string[] = [],
|
|
108
|
+
): Promise<Record<string, any>> {
|
|
109
|
+
const record: Record<string, any> = {};
|
|
110
|
+
|
|
111
|
+
for (const field of fields) {
|
|
112
|
+
// 1. Handle Layout Fields (Recursive)
|
|
113
|
+
if (field.type === "row" || field.type === "collapsible") {
|
|
114
|
+
Object.assign(record, await generateDataForFields(field.fields, db, locales));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (field.type === "tabs") {
|
|
119
|
+
for (const tab of field.tabs) {
|
|
120
|
+
Object.assign(record, await generateDataForFields(tab.fields, db, locales));
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Handle Group (Nested)
|
|
126
|
+
if (field.type === "group" && field.name) {
|
|
127
|
+
record[field.name] = await generateDataForFields(field.fields, db, locales);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Handle Normal Fields with name
|
|
132
|
+
if (field.name) {
|
|
133
|
+
const isLocalized = !!(field as any).localized && locales.length > 0;
|
|
134
|
+
|
|
135
|
+
const generateFieldValue = async () => {
|
|
136
|
+
// Handle Relationships
|
|
137
|
+
if (field.type === "relationship" && "relationTo" in field) {
|
|
138
|
+
if (field.hasMany) {
|
|
139
|
+
const count = faker.number.int({ min: 1, max: 3 });
|
|
140
|
+
return await getRandomIds(db, field.relationTo, count);
|
|
141
|
+
}
|
|
142
|
+
const ids = await getRandomIds(db, field.relationTo, 1);
|
|
143
|
+
return ids[0] || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle Blocks
|
|
147
|
+
if (field.type === "blocks" && field.blocks) {
|
|
148
|
+
const blockCount = faker.number.int({ min: 1, max: 3 });
|
|
149
|
+
const generatedBlocks = [];
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < blockCount; i++) {
|
|
152
|
+
const blockType =
|
|
153
|
+
field.blocks[faker.number.int({ min: 0, max: field.blocks.length - 1 })];
|
|
154
|
+
if (!blockType) continue;
|
|
155
|
+
|
|
156
|
+
const blockData = await generateDataForFields(blockType.fields, db, locales);
|
|
157
|
+
generatedBlocks.push({
|
|
158
|
+
...blockData,
|
|
159
|
+
blockType: blockType.slug,
|
|
160
|
+
id: faker.string.uuid(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return generatedBlocks;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Default Generators
|
|
168
|
+
const generator = defaultFieldGenerators[field.type];
|
|
169
|
+
if (generator) {
|
|
170
|
+
if (field.type === "select" || field.type === "radio") {
|
|
171
|
+
const options = (field as any).options;
|
|
172
|
+
const choices = options?.choices || [];
|
|
173
|
+
if (choices.length > 0) {
|
|
174
|
+
const choice = choices[Math.floor(Math.random() * choices.length)];
|
|
175
|
+
return typeof choice === "string" ? choice : choice.value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return generator();
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (isLocalized) {
|
|
184
|
+
const localizedValue: Record<string, any> = {};
|
|
185
|
+
for (const locale of locales) {
|
|
186
|
+
localizedValue[locale] = await generateFieldValue();
|
|
187
|
+
}
|
|
188
|
+
record[field.name] = localizedValue;
|
|
189
|
+
} else {
|
|
190
|
+
record[field.name] = await generateFieldValue();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return record;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generates a single record for a collection.
|
|
200
|
+
*/
|
|
201
|
+
export async function generateRecord(db: DatabaseAdapter, collection: Collection, locales: string[] = []) {
|
|
202
|
+
return generateDataForFields(collection.fields as Field[], db, locales);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Topologically sorts collections based on their dependencies.
|
|
207
|
+
*/
|
|
208
|
+
export function sortCollections(collections: Collection[]): Collection[] {
|
|
209
|
+
const sorted: Collection[] = [];
|
|
210
|
+
const visited = new Set<string>();
|
|
211
|
+
const visiting = new Set<string>();
|
|
212
|
+
|
|
213
|
+
const visit = (collection: Collection) => {
|
|
214
|
+
if (visited.has(collection.slug)) return;
|
|
215
|
+
if (visiting.has(collection.slug)) {
|
|
216
|
+
throw new Error(`Circular dependency detected: ${collection.slug}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
visiting.add(collection.slug);
|
|
220
|
+
|
|
221
|
+
// Implicit dependencies from relationships
|
|
222
|
+
const deps: string[] = [];
|
|
223
|
+
|
|
224
|
+
// Recursive search for relationships
|
|
225
|
+
const findDeps = (fields: Field[]) => {
|
|
226
|
+
for (const f of fields) {
|
|
227
|
+
if (f.type === "relationship") {
|
|
228
|
+
deps.push((f as any).relationTo);
|
|
229
|
+
} else if (f.type === "blocks" && f.blocks) {
|
|
230
|
+
f.blocks.forEach((b) => findDeps(b.fields as Field[]));
|
|
231
|
+
} else if ("fields" in f && f.fields) {
|
|
232
|
+
findDeps(f.fields as Field[]);
|
|
233
|
+
} else if ("tabs" in f && f.tabs) {
|
|
234
|
+
f.tabs.forEach((t) => findDeps(t.fields as Field[]));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
findDeps(collection.fields as Field[]);
|
|
239
|
+
|
|
240
|
+
for (const depSlug of deps) {
|
|
241
|
+
const depColl = collections.find((c) => c.slug === depSlug);
|
|
242
|
+
if (depColl) visit(depColl);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
visiting.delete(collection.slug);
|
|
246
|
+
visited.add(collection.slug);
|
|
247
|
+
sorted.push(collection);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
for (const collection of collections) {
|
|
251
|
+
visit(collection);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return sorted;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Seeds the database with mock data.
|
|
259
|
+
*/
|
|
260
|
+
export async function autoSeed(
|
|
261
|
+
config: OpacaConfig,
|
|
262
|
+
countPerCollection = 10,
|
|
263
|
+
reset = false,
|
|
264
|
+
type: "collections" | "assets" | "all" = "all",
|
|
265
|
+
) {
|
|
266
|
+
const { collections, db, globals, storages, serverURL } = config;
|
|
267
|
+
|
|
268
|
+
console.log(`๐ฑ Starting automatic seed (${countPerCollection} records per collection)...`);
|
|
269
|
+
|
|
270
|
+
// Include system collections (like _opaca_assets)
|
|
271
|
+
const { getSystemCollections } = await import("../db/system-schema.js");
|
|
272
|
+
const systemCollections = getSystemCollections().filter((c) => c.slug === "_opaca_assets");
|
|
273
|
+
const allCollections = [...systemCollections, ...collections];
|
|
274
|
+
|
|
275
|
+
let collectionsToSeed = sortCollections(allCollections);
|
|
276
|
+
|
|
277
|
+
if (type === "assets") {
|
|
278
|
+
collectionsToSeed = collectionsToSeed.filter((c) => c.slug === "_opaca_assets");
|
|
279
|
+
console.log("๐ Seeding only assets...");
|
|
280
|
+
} else if (type === "collections") {
|
|
281
|
+
collectionsToSeed = collectionsToSeed.filter((c) => c.slug !== "_opaca_assets");
|
|
282
|
+
console.log("๐ Seeding only user collections...");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Connect and ensure adapter knows the schema (pass ALL collections for proper whitelisting)
|
|
286
|
+
await db.connect();
|
|
287
|
+
await db.migrate(allCollections, globals || []);
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
if (reset) {
|
|
291
|
+
console.log("๐งน Resetting data (deleting existing records)...");
|
|
292
|
+
// Delete in reverse order to respect foreign key constraints if any
|
|
293
|
+
const reversed = [...collectionsToSeed].reverse();
|
|
294
|
+
for (const collection of reversed) {
|
|
295
|
+
console.log(`Cleaning ${collection.slug}...`);
|
|
296
|
+
await db.deleteMany?.(collection.slug, {});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const storageAdapter = (storages as any)?.default || storages;
|
|
301
|
+
|
|
302
|
+
const locales = config.i18n?.locales || [];
|
|
303
|
+
|
|
304
|
+
for (const collection of collectionsToSeed) {
|
|
305
|
+
console.log(`Seeding ${collection.slug}...`);
|
|
306
|
+
|
|
307
|
+
const isAssetCollection = collection.slug === "_opaca_assets";
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < countPerCollection; i++) {
|
|
310
|
+
let data: any;
|
|
311
|
+
|
|
312
|
+
if (isAssetCollection) {
|
|
313
|
+
// ... (asset logic remains the same)
|
|
314
|
+
const id = faker.string.uuid();
|
|
315
|
+
const width = faker.number.int({ min: 400, max: 1200 });
|
|
316
|
+
const height = faker.number.int({ min: 300, max: 800 });
|
|
317
|
+
const color = faker.color.rgb({ prefix: "" });
|
|
318
|
+
const textColor = faker.color.rgb({ prefix: "" });
|
|
319
|
+
|
|
320
|
+
// Randomize between Picsum and Placehold.co
|
|
321
|
+
const usePicsum = Math.random() > 0.5;
|
|
322
|
+
let imageUrl = "";
|
|
323
|
+
|
|
324
|
+
if (usePicsum) {
|
|
325
|
+
const categories = ["nature", "city", "tech", "people", "animals", "architecture"];
|
|
326
|
+
const category = faker.helpers.arrayElement(categories);
|
|
327
|
+
imageUrl = `https://picsum.photos/seed/${category}-${id}/${width}/${height}`;
|
|
328
|
+
} else {
|
|
329
|
+
imageUrl = `https://placehold.co/${width}x${height}/${color}/${textColor}.png?text=Seed+${i}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 1. Fetch real image
|
|
333
|
+
const res = await fetch(imageUrl);
|
|
334
|
+
if (!res.ok) {
|
|
335
|
+
throw new Error(`Failed to fetch placeholder image: ${imageUrl}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
339
|
+
const mime_type = res.headers.get("content-type")?.split(";")[0] || "image/png";
|
|
340
|
+
|
|
341
|
+
const extMap: Record<string, string> = {
|
|
342
|
+
"image/jpeg": "jpg",
|
|
343
|
+
"image/png": "png",
|
|
344
|
+
"image/webp": "webp",
|
|
345
|
+
"image/gif": "gif",
|
|
346
|
+
"image/svg+xml": "svg",
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const ext = extMap[mime_type] || "png";
|
|
350
|
+
|
|
351
|
+
// 2. Build FileRecord
|
|
352
|
+
const fileRecord = {
|
|
353
|
+
filename: `seed-image-${i}.${ext}`,
|
|
354
|
+
original_filename: `seed-image-${i}.${ext}`,
|
|
355
|
+
mime_type,
|
|
356
|
+
filesize: arrayBuffer.byteLength,
|
|
357
|
+
buffer: new Uint8Array(arrayBuffer),
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// 3. Upload via adapter
|
|
361
|
+
if (!storageAdapter || typeof storageAdapter.upload !== "function") {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"Storage adapter is required for seeding assets and must have an 'upload' method.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const uploaded = await storageAdapter.upload(fileRecord, {
|
|
368
|
+
generateUniqueName: true,
|
|
369
|
+
customMetadata: {
|
|
370
|
+
sourceUrl: imageUrl,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// 4. Persist metadata real
|
|
375
|
+
data = {
|
|
376
|
+
id,
|
|
377
|
+
key: uploaded.filename,
|
|
378
|
+
filename: uploaded.filename,
|
|
379
|
+
originalFilename: fileRecord.original_filename,
|
|
380
|
+
mimeType: uploaded.mime_type,
|
|
381
|
+
filesize: uploaded.filesize,
|
|
382
|
+
width,
|
|
383
|
+
height,
|
|
384
|
+
bucket: "default",
|
|
385
|
+
url: uploaded.url,
|
|
386
|
+
thumbnailUrl: uploaded.url, // (placeholder)
|
|
387
|
+
altText: faker.lorem.sentence(),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// LOG THE FULL URL
|
|
391
|
+
const baseURL = serverURL || "http://localhost:8787";
|
|
392
|
+
console.log(`[Asset] Source: ${imageUrl}`);
|
|
393
|
+
console.log(`[Asset] Seeded: ${baseURL}/api/assets/${id}/view (${uploaded.filename})`);
|
|
394
|
+
} else {
|
|
395
|
+
data = await generateRecord(db, collection, locales);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await db.create(collection.slug, data);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
console.log("โ
Seeding completed.");
|
|
402
|
+
} finally {
|
|
403
|
+
await db.disconnect();
|
|
404
|
+
}
|
|
405
|
+
}
|