@acmekit/acmekit 2.13.89 → 2.13.91

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.
@@ -867,7 +867,17 @@ import { sdk } from "../../../../lib/sdk"
867
867
  type Currency = { code: string; name: string }
868
868
  const columnHelper = createDataTableColumnHelper<Currency>()
869
869
 
870
+ // Page component — renders RouteFocusModal, delegates form to child
870
871
  const AddCurrenciesPage = () => {
872
+ return (
873
+ <RouteFocusModal>
874
+ <AddCurrenciesForm />
875
+ </RouteFocusModal>
876
+ )
877
+ }
878
+
879
+ // Form component — useRouteModal() is valid here (inside RouteFocusModal)
880
+ const AddCurrenciesForm = () => {
871
881
  const queryClient = useQueryClient()
872
882
  const { handleSuccess } = useRouteModal()
873
883
 
@@ -924,37 +934,35 @@ const AddCurrenciesPage = () => {
924
934
  })
925
935
 
926
936
  return (
927
- <RouteFocusModal>
928
- <RouteFocusModal.Form form={form}>
929
- <KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
930
- <RouteFocusModal.Header>
931
- <div className="flex flex-1 items-center justify-between">
932
- {form.formState.errors.currencies && (
933
- <Hint variant="error">{form.formState.errors.currencies.message}</Hint>
934
- )}
935
- </div>
936
- </RouteFocusModal.Header>
937
- <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
938
- <DataTable instance={table}>
939
- <DataTable.Table emptyState={{
940
- empty: { heading: "No currencies available" },
941
- }} />
942
- <DataTable.Pagination />
943
- </DataTable>
944
- </RouteFocusModal.Body>
945
- <RouteFocusModal.Footer>
946
- <div className="flex items-center justify-end gap-x-2">
947
- <RouteFocusModal.Close asChild>
948
- <Button size="small" variant="secondary">Cancel</Button>
949
- </RouteFocusModal.Close>
950
- <Button size="small" type="submit" isLoading={isPending}>
951
- Save
952
- </Button>
953
- </div>
954
- </RouteFocusModal.Footer>
955
- </KeyboundForm>
956
- </RouteFocusModal.Form>
957
- </RouteFocusModal>
937
+ <RouteFocusModal.Form form={form}>
938
+ <KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
939
+ <RouteFocusModal.Header>
940
+ <div className="flex flex-1 items-center justify-between">
941
+ {form.formState.errors.currencies && (
942
+ <Hint variant="error">{form.formState.errors.currencies.message}</Hint>
943
+ )}
944
+ </div>
945
+ </RouteFocusModal.Header>
946
+ <RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
947
+ <DataTable instance={table}>
948
+ <DataTable.Table emptyState={{
949
+ empty: { heading: "No currencies available" },
950
+ }} />
951
+ <DataTable.Pagination />
952
+ </DataTable>
953
+ </RouteFocusModal.Body>
954
+ <RouteFocusModal.Footer>
955
+ <div className="flex items-center justify-end gap-x-2">
956
+ <RouteFocusModal.Close asChild>
957
+ <Button size="small" variant="secondary">Cancel</Button>
958
+ </RouteFocusModal.Close>
959
+ <Button size="small" type="submit" isLoading={isPending}>
960
+ Save
961
+ </Button>
962
+ </div>
963
+ </RouteFocusModal.Footer>
964
+ </KeyboundForm>
965
+ </RouteFocusModal.Form>
958
966
  )
959
967
  }
960
968
 
@@ -422,6 +422,34 @@ const SettingsPage = () => {
422
422
  }
423
423
  ```
424
424
 
425
+ ### Never Install react-hook-form, @hookform/resolvers, or zod Directly
426
+
427
+ These packages are **provided by AcmeKit** at specific pinned versions. Installing your own version causes version conflicts:
428
+
429
+ - `@hookform/resolvers@5.x` requires `zod/v4/core` (Zod v4) — AcmeKit ships Zod v3 → **build fails** with `Could not resolve "zod/v4/core"`
430
+ - `react-hook-form` dual instances → `useFormContext()` returns `null` → form fields crash
431
+ - Bare `zod` import → **build fails** in plugins with `Could not resolve "zod"`
432
+
433
+ **For plugins**: `react-hook-form` and `@hookform/resolvers` go in `peerDependencies` + `devDependencies` only (never `dependencies`). Match the host app's versions.
434
+
435
+ **For apps**: Do not add these packages at all — they come from `@acmekit/ui`.
436
+
437
+ **Imports**:
438
+ - Admin UI: `import * as zod from "@acmekit/deps/zod"` — never bare `"zod"`
439
+ - Backend: `import { z } from "@acmekit/framework/zod"` — never bare `"zod"`
440
+ - `useForm` from `react-hook-form` and `zodResolver` from `@hookform/resolvers/zod` — these are provided by the host
441
+
442
+ ### Vite "Outdated Optimize Dep" 504 Errors
443
+
444
+ After changing plugin dependencies, Vite's pre-bundled dependency cache becomes stale. The browser shows `504 (Outdated Optimize Dep)` for multiple modules.
445
+
446
+ **Fix**: Stop the dev server, delete `.vite/deps/` in the host app's `node_modules/`, and restart:
447
+
448
+ ```bash
449
+ rm -rf node_modules/.vite/deps
450
+ # restart dev server
451
+ ```
452
+
425
453
  ---
426
454
 
427
455
  ## Key Rules
@@ -42,7 +42,7 @@ Co-locate validation, query config, response schemas, rate limiting, and RBAC po
42
42
  When extending generated CRUD routes, keep the existing `query-config.ts` / `helpers.ts` structure and only adjust fields, filters, workflow calls, or refetch logic.
43
43
 
44
44
  ```typescript
45
- import { z } from "zod"
45
+ import { z } from "@acmekit/framework/zod"
46
46
  import {
47
47
  defineRouteConfig,
48
48
  AuthenticatedAcmeKitTypedRequest,
@@ -532,8 +532,9 @@ createProductsWorkflow.hooks.productsCreated(
532
532
 
533
533
  ## Zod Import Note
534
534
 
535
- - In route files (`defineRouteConfig`): `import { z } from "zod"` works
536
- - In `middlewares.ts` (`additionalDataValidator`): use `import { z } from "@acmekit/framework/zod"`
535
+ - **Always** use `import { z } from "@acmekit/framework/zod"` — never bare `"zod"`
536
+ - Bare `"zod"` may resolve in apps but **fails in plugins** (zod is not a direct dependency)
537
+ - This applies to both route files and `middlewares.ts`
537
538
 
538
539
  ## Common Imports
539
540
 
@@ -546,8 +547,7 @@ import {
546
547
  defineMiddlewares,
547
548
  AcmeKitNextFunction,
548
549
  } from "@acmekit/framework/http"
549
- import { z } from "zod"
550
- import { z as frameworkZod } from "@acmekit/framework/zod" // for middlewares.ts
550
+ import { z } from "@acmekit/framework/zod"
551
551
  import { ContainerRegistrationKeys, AcmeKitError } from "@acmekit/framework/utils"
552
552
  ```
553
553
 
@@ -737,11 +737,10 @@ export const POST = async (req, res) => {
737
737
  // RIGHT — use :param format
738
738
  { matcher: "/custom/:id", middlewares: [...] } // ✅
739
739
 
740
- // WRONG — using bare zod in middlewares.ts
741
- import { z } from "zod" // ❌ in middlewares.ts
742
- additionalDataValidator: { brand: z.string() }
740
+ // WRONG — bare "zod" import (fails in plugins, not a direct dependency)
741
+ import { z } from "zod" // ❌ never use bare zod
743
742
 
744
- // RIGHT — use framework zod in middlewares.ts
743
+ // RIGHT — always use framework re-export
745
744
  import { z } from "@acmekit/framework/zod" // ✅
746
745
 
747
746
  // WRONG — no .strict() on body schema (allows extra fields through)
@@ -772,4 +771,26 @@ const { enabled, ...filters } = req.validatedQuery // ❌ crashes if request ha
772
771
 
773
772
  // RIGHT — always provide fallback when destructuring validatedQuery
774
773
  const { enabled, ...filters } = req.validatedQuery ?? {} // ✅ safe when no query params
774
+
775
+ // WRONG — seeding/syncing data inside a route handler (runs on every request)
776
+ export const GET = async (req, res) => {
777
+ const service = req.scope.resolve(MY_MODULE)
778
+ await service.syncProviders() // ❌ data mutation on every GET — slow, side-effectful
779
+ const data = await query.graph({ entity: "my_entity" })
780
+ res.json({ items: data })
781
+ }
782
+
783
+ // RIGHT — routes are pure query/mutation handlers, no seeding or syncing
784
+ // Seed data via: createProvidersLoader onRegistered, onPluginStart, or scripts
785
+ export const GET = async (req, res) => {
786
+ const data = await query.graph({ entity: "my_entity" })
787
+ res.json({ items: data }) // ✅ just reads data
788
+ }
789
+
790
+ // WRONG — importing service class directly in route file for type
791
+ import type MyModuleService from "../../../modules/my/service" // ❌ tight coupling
792
+ const svc = req.scope.resolve<MyModuleService>(MY_MODULE)
793
+
794
+ // RIGHT — resolve via module constant, type-cast loosely if needed
795
+ const svc: any = req.scope.resolve(MY_MODULE) // ✅
775
796
  ```
@@ -97,7 +97,7 @@ export const BLOG_MODULE = "blog"
97
97
  export default Module(BLOG_MODULE, { service: BlogModuleService })
98
98
  ```
99
99
 
100
- The generator auto-registers modules in `acmekit-config.ts`. Verify registration after generation and preserve the generated constant naming.
100
+ The generator auto-registers modules in `acmekit-config.ts`. Verify registration after generation.
101
101
 
102
102
  ## Constructor Pattern
103
103
 
@@ -192,7 +192,7 @@ export default class MyModuleService {
192
192
 
193
193
  **When to use loaders:** Register external connections at startup; validate required options (throw on missing to prevent startup). Loaders are idempotent — check `container.hasRegistration("key")` before re-registering.
194
194
 
195
- **Don't use loaders for:** Scheduled tasks → use jobs. Event-triggered logic → use subscribers.
195
+ **Don't use loaders for:** Scheduled tasks → use jobs. Event-triggered logic → use subscribers. Database mutations (INSERT/UPDATE/DELETE) → use scripts, subscribers, or seed data (exception: `onRegistered` in `createProvidersLoader` is designed for DB seeding via service methods). Never resolve `PG_CONNECTION` for raw SQL in loaders — use service methods instead.
196
196
 
197
197
  ## Module Options Validation
198
198
 
@@ -262,6 +262,73 @@ export default Module(RELAYER_MODULE, {
262
262
  })
263
263
  ```
264
264
 
265
+ ### createProvidersLoader — Full API
266
+
267
+ `createProvidersLoader` accepts a `ProvidersConfig` object with these options:
268
+
269
+ | Option | Required | Default | Purpose |
270
+ |--------|----------|---------|---------|
271
+ | `prefix` | Yes | — | Container registration key prefix (e.g., `"pp_"`, `"cr_"`) |
272
+ | `identifiersKey` | Yes | — | Key for tracking registered provider IDs |
273
+ | `moduleName` | No | `"module"` | Label for log/error messages |
274
+ | `keyFormat` | No | `"prefixId"` | `"prefixId"` → key=`{prefix}{id}`, or `"prefixIdentifierId"` → key=`{prefix}{identifier}_{id}` |
275
+ | `defaults` | No | `[]` | Built-in providers shipped with the module (see below) |
276
+ | `defaultProviderKey` | No | — | Container key to register the elected default provider ID |
277
+ | `requireDefault` | No | `false` | Throw at startup if no default provider is found |
278
+ | `onRegistered` | No | — | Async hook called after all providers are registered |
279
+
280
+ **`defaults`** — each entry is a `DefaultProviderConfig`:
281
+
282
+ ```typescript
283
+ {
284
+ service: ProviderClass, // must have static `identifier`
285
+ id: string, // registration ID
286
+ options?: Record | ((moduleOptions) => Record), // static or derived from module options
287
+ condition?: (moduleOptions) => boolean, // skip this default if returns false
288
+ }
289
+ ```
290
+
291
+ **`onRegistered`** — receives `{ container, providers, registeredDefaults }`:
292
+
293
+ ```typescript
294
+ onRegistered: async ({ container, providers, registeredDefaults }) => {
295
+ // container: module-scoped AcmeKitContainer (module's own service is resolvable here)
296
+ // providers: user-configured ModuleProvider[] from config
297
+ // registeredDefaults: Array<{ id, options }> — only defaults that passed condition
298
+ }
299
+ ```
300
+
301
+ **Example with defaults and onRegistered** (from core modules):
302
+
303
+ ```typescript
304
+ createProvidersLoader({
305
+ prefix: PAYMENT_PROVIDER_PREFIX,
306
+ identifiersKey: PAYMENT_PROVIDER_IDENTIFIER_KEY,
307
+ moduleName: PAYMENT_MODULE,
308
+ defaults: [
309
+ {
310
+ service: SystemProvider,
311
+ id: "system",
312
+ condition: (opts) => opts.enableSystemProvider !== false,
313
+ },
314
+ ],
315
+ onRegistered: async ({ container, registeredDefaults }) => {
316
+ // Sync DB rows for registered providers via service methods
317
+ const service: any = container.resolve(PAYMENT_MODULE)
318
+ for (const provider of registeredDefaults) {
319
+ const existing = await service.listPaymentProviders({ id: [provider.id] })
320
+ if (!existing.length) {
321
+ await service.createPaymentProviders([
322
+ { id: provider.id, is_enabled: true },
323
+ ])
324
+ }
325
+ }
326
+ },
327
+ })
328
+ ```
329
+
330
+ **Important**: Even inside `onRegistered`, always use service methods for DB mutations — never raw SQL via `PG_CONNECTION`.
331
+
265
332
  ### Service (resolving providers)
266
333
 
267
334
  Use the leading `TProvider` generic to get a typed `this.providers` registry.
@@ -767,4 +834,54 @@ AcmeKitService({ providers: { prefix: "cr_", identifiersKey: "chain_relayer_prov
767
834
 
768
835
  // RIGHT — define constants once in constants.ts, import everywhere
769
836
  import { CHAIN_RELAYER_PROVIDER_PREFIX, CHAIN_RELAYER_IDENTIFIER_KEY } from "./constants"
837
+
838
+ // WRONG — using raw SQL / PG_CONNECTION for data mutations
839
+ const pgConnection = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
840
+ await pgConnection.raw(
841
+ `INSERT INTO payment_provider (id) VALUES (?)`, // ❌ bypasses ORM, service layer, events
842
+ [id]
843
+ )
844
+
845
+ // RIGHT — use the module's service methods for all data mutations
846
+ const service = container.resolve(PAYMENT_MODULE)
847
+ await service.createPaymentProviders([{ id: "pp_system", is_enabled: true }]) // ✅
848
+
849
+ // WRONG — using raw SQL in onRegistered to seed provider rows
850
+ onRegistered: async ({ container, registeredDefaults }) => {
851
+ const pg = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
852
+ await pg.raw(`INSERT INTO ...`, [id]) // ❌ bypasses ORM
853
+ }
854
+
855
+ // RIGHT — use service methods in onRegistered
856
+ onRegistered: async ({ container, registeredDefaults }) => {
857
+ const service: any = container.resolve(MY_MODULE)
858
+ for (const provider of registeredDefaults) {
859
+ const existing = await service.listMyProviders({ id: [provider.id] })
860
+ if (!existing.length) {
861
+ await service.createMyProviders([{ id: provider.id, is_enabled: true }]) // ✅
862
+ }
863
+ }
864
+ }
865
+
866
+ // WRONG — lazy-syncing / seeding data inside a GET route handler
867
+ export const GET = async (req, res) => {
868
+ const service = req.scope.resolve(PAYMENT_MODULE)
869
+ await service.syncRegisteredProviders() // ❌ runs on EVERY request
870
+ const { data } = await query.graph({ entity: "payment_provider" })
871
+ res.json({ items: data })
872
+ }
873
+
874
+ // RIGHT — seed data once at startup (onPluginStart) or via script, route just queries
875
+ export const GET = async (req, res) => {
876
+ const { data } = await query.graph({ entity: "payment_provider" })
877
+ res.json({ items: data }) // ✅ pure query, no side effects
878
+ }
879
+
880
+ // WRONG — importing service class directly in route file
881
+ import type PaymentModuleService from "../../../modules/payment/service" // ❌
882
+ const service = req.scope.resolve<PaymentModuleService>(PAYMENT_MODULE)
883
+
884
+ // RIGHT — resolve with module constant, cast to interface if needed
885
+ const service: any = req.scope.resolve(PAYMENT_MODULE) // ✅
886
+ // or use the generated interface type from @types if available
770
887
  ```
@@ -497,6 +497,7 @@ src/admin/routes/<feature>/
497
497
  - Fields: `grid grid-cols-1 gap-4 md:grid-cols-2`
498
498
  - Footer: `<div className="flex items-center justify-end gap-x-2">`, submit `type="submit"`
499
499
  - All buttons `size="small"`, use `useRouteModal()` for `handleSuccess`
500
+ - **IMPORTANT**: `useRouteModal()` must be in a **child component** rendered inside `<RouteFocusModal>` — split into page (renders modal) + form (calls hook)
500
501
 
501
502
  **Edit form** (`[id]/edit/page.tsx`):
502
503
  - `<RouteDrawer>` (auto-opens, navigates back on close)
@@ -505,6 +506,7 @@ src/admin/routes/<feature>/
505
506
  - Body fields: single column `flex flex-col gap-y-4` (drawer is narrow)
506
507
  - Footer: same as create
507
508
  - Loading guard wraps the `<KeyboundForm>`, not just body content
509
+ - **IMPORTANT**: `useRouteModal()` must be in a **child component** rendered inside `<RouteDrawer>` — split into page (renders drawer) + form (calls hook)
508
510
 
509
511
  #### Multi-Entity Feature
510
512
 
@@ -13,6 +13,7 @@ You are a senior AcmeKit framework developer. Build features using code generato
13
13
  - Use string literals for container keys — use `ContainerRegistrationKeys.*` and `Modules.*`
14
14
  - Call services directly for mutations in routes — use workflows
15
15
  - Put business logic in workflow steps — steps are thin wrappers; service methods own orchestration
16
+ - Use raw SQL via `PG_CONNECTION` / `pgConnection.raw()` for data mutations — use service methods
16
17
 
17
18
  ### Ask First
18
19