@donotdev/crud 0.0.14 → 0.0.16

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.
Files changed (238) hide show
  1. package/README.md +27 -19
  2. package/dist/CrudService.d.ts +84 -11
  3. package/dist/CrudService.d.ts.map +1 -1
  4. package/dist/CrudService.js +9 -1
  5. package/dist/CrudStore.d.ts.map +1 -1
  6. package/dist/CrudStore.js +1 -1
  7. package/dist/FieldRegistry.d.ts +43 -43
  8. package/dist/FieldRegistry.d.ts.map +1 -1
  9. package/dist/FieldRegistry.js +1 -1
  10. package/dist/adapters/FunctionsAdapter.d.ts +20 -26
  11. package/dist/adapters/FunctionsAdapter.d.ts.map +1 -1
  12. package/dist/adapters/FunctionsAdapter.js +1 -1
  13. package/dist/adapters/index.d.ts +0 -1
  14. package/dist/adapters/index.d.ts.map +1 -1
  15. package/dist/adapters/index.js +1 -1
  16. package/dist/components/CrudButton.js +1 -1
  17. package/dist/components/CrudCard.d.ts +8 -0
  18. package/dist/components/CrudCard.d.ts.map +1 -0
  19. package/dist/components/CrudCard.js +1 -0
  20. package/dist/components/DateFilter.d.ts.map +1 -1
  21. package/dist/components/DateFilter.js +1 -1
  22. package/dist/components/DisplayFieldRenderer.js +1 -1
  23. package/dist/components/DisplayThumbnail.d.ts +29 -0
  24. package/dist/components/DisplayThumbnail.d.ts.map +1 -0
  25. package/dist/components/DisplayThumbnail.js +1 -0
  26. package/dist/components/EntityFilters.d.ts.map +1 -1
  27. package/dist/components/EntityFilters.js +1 -1
  28. package/dist/components/FormFieldRenderer.js +1 -1
  29. package/dist/components/FormLayout.d.ts.map +1 -1
  30. package/dist/components/FormLayout.js +1 -1
  31. package/dist/components/__tests__/EntityFilters.test.d.ts +2 -0
  32. package/dist/components/__tests__/EntityFilters.test.d.ts.map +1 -0
  33. package/dist/components/__tests__/EntityFilters.test.js +1 -0
  34. package/dist/components/__tests__/FormFieldRenderer.test.d.ts +2 -0
  35. package/dist/components/__tests__/FormFieldRenderer.test.d.ts.map +1 -0
  36. package/dist/components/__tests__/FormFieldRenderer.test.js +1 -0
  37. package/dist/components/controlled/complex/ControlledAddressField.js +1 -1
  38. package/dist/components/controlled/complex/ControlledDateField.js +1 -1
  39. package/dist/components/controlled/complex/ControlledGeoPointField.js +1 -1
  40. package/dist/components/controlled/complex/ControlledMapField.js +1 -1
  41. package/dist/components/controlled/complex/ControlledMultiInputField.js +1 -1
  42. package/dist/components/controlled/complex/ControlledRichTextField.js +1 -1
  43. package/dist/components/controlled/complex/ControlledTimestampField.js +1 -1
  44. package/dist/components/controlled/complex/index.js +1 -1
  45. package/dist/components/controlled/file/ControlledDocumentField.js +1 -1
  46. package/dist/components/controlled/file/ControlledFileField.js +1 -1
  47. package/dist/components/controlled/file/ControlledImageField.js +1 -1
  48. package/dist/components/controlled/file/ControlledMultiDocumentField.js +1 -1
  49. package/dist/components/controlled/file/ControlledMultiFileField.js +1 -1
  50. package/dist/components/controlled/file/ControlledMultiImageField.js +1 -1
  51. package/dist/components/controlled/file/index.js +1 -1
  52. package/dist/components/controlled/index.js +1 -1
  53. package/dist/components/controlled/input/ControlledCheckboxField.js +1 -1
  54. package/dist/components/controlled/input/ControlledCurrencyField.js +1 -1
  55. package/dist/components/controlled/input/ControlledDurationField.js +1 -1
  56. package/dist/components/controlled/input/ControlledGdprConsentField.js +1 -1
  57. package/dist/components/controlled/input/ControlledNumberField.js +1 -1
  58. package/dist/components/controlled/input/ControlledPasswordField.js +1 -1
  59. package/dist/components/controlled/input/ControlledPhoneField.js +1 -1
  60. package/dist/components/controlled/input/ControlledPriceField.js +1 -1
  61. package/dist/components/controlled/input/ControlledRangeField.js +1 -1
  62. package/dist/components/controlled/input/ControlledRatingField.js +1 -1
  63. package/dist/components/controlled/input/ControlledSwitchField.js +1 -1
  64. package/dist/components/controlled/input/ControlledTextField.js +1 -1
  65. package/dist/components/controlled/input/ControlledTextareaField.js +1 -1
  66. package/dist/components/controlled/input/index.js +1 -1
  67. package/dist/components/controlled/select/ControlledComboboxField.js +1 -1
  68. package/dist/components/controlled/select/ControlledDropdownField.js +1 -1
  69. package/dist/components/controlled/select/ControlledMultiDropdownField.js +1 -1
  70. package/dist/components/controlled/select/ControlledRadioField.js +1 -1
  71. package/dist/components/controlled/select/ControlledYearField.js +1 -1
  72. package/dist/components/controlled/select/index.js +1 -1
  73. package/dist/components/controlled/types.d.ts +19 -5
  74. package/dist/components/controlled/types.d.ts.map +1 -1
  75. package/dist/components/controlled/types.js +1 -1
  76. package/dist/components/fields/display/AvatarFieldDisplay.js +1 -1
  77. package/dist/components/fields/display/BadgeFieldDisplay.js +1 -1
  78. package/dist/components/fields/display/ButtonFieldDisplay.js +1 -1
  79. package/dist/components/fields/display/CheckboxFieldDisplay.js +1 -1
  80. package/dist/components/fields/display/DateFieldDisplay.js +1 -1
  81. package/dist/components/fields/display/DropdownDisplay.js +1 -1
  82. package/dist/components/fields/display/FileFieldDisplay.js +1 -1
  83. package/dist/components/fields/display/GeoPointFieldDisplay.js +1 -1
  84. package/dist/components/fields/display/HiddenFieldDisplay.js +1 -1
  85. package/dist/components/fields/display/ImageFieldDisplay.js +1 -1
  86. package/dist/components/fields/display/LinkFieldDisplay.js +1 -1
  87. package/dist/components/fields/display/MapFieldDisplay.js +1 -1
  88. package/dist/components/fields/display/MultiDropdownDisplay.js +1 -1
  89. package/dist/components/fields/display/MultiInputTextFieldDisplay.js +1 -1
  90. package/dist/components/fields/display/NumberFieldDisplay.js +1 -1
  91. package/dist/components/fields/display/PasswordFieldDisplay.js +1 -1
  92. package/dist/components/fields/display/PhoneNumberDisplay.js +1 -1
  93. package/dist/components/fields/display/RadioFieldDisplay.js +1 -1
  94. package/dist/components/fields/display/RangeFieldDisplay.js +1 -1
  95. package/dist/components/fields/display/ReferenceFieldDisplay.js +1 -1
  96. package/dist/components/fields/display/RichTextDisplay.d.ts.map +1 -1
  97. package/dist/components/fields/display/RichTextDisplay.js +2 -2
  98. package/dist/components/fields/display/TextAreaDisplay.js +1 -1
  99. package/dist/components/fields/display/TextFieldDisplay.js +1 -1
  100. package/dist/components/fields/display/TimestampFieldDisplay.js +1 -1
  101. package/dist/components/fields/display/index.js +1 -1
  102. package/dist/components/form/fields/AddressFieldComponent.d.ts.map +1 -1
  103. package/dist/components/form/fields/AddressFieldComponent.js +1 -1
  104. package/dist/components/form/fields/AvatarFieldComponent.js +1 -1
  105. package/dist/components/form/fields/BadgeFieldComponent.js +1 -1
  106. package/dist/components/form/fields/ButtonFieldComponent.js +1 -1
  107. package/dist/components/form/fields/CheckboxFieldComponent.js +1 -1
  108. package/dist/components/form/fields/ComboboxComponent.js +1 -1
  109. package/dist/components/form/fields/CurrencyFieldComponent.js +1 -1
  110. package/dist/components/form/fields/DateFieldComponent.js +1 -1
  111. package/dist/components/form/fields/DocumentFieldComponent.d.ts.map +1 -1
  112. package/dist/components/form/fields/DocumentFieldComponent.js +1 -1
  113. package/dist/components/form/fields/DropdownComponent.js +1 -1
  114. package/dist/components/form/fields/DurationFieldComponent.js +1 -1
  115. package/dist/components/form/fields/FileFieldComponent.js +1 -1
  116. package/dist/components/form/fields/GdprConsentFieldComponent.js +1 -1
  117. package/dist/components/form/fields/GeoPointFieldComponent.js +1 -1
  118. package/dist/components/form/fields/HiddenFieldComponent.js +1 -1
  119. package/dist/components/form/fields/ImageFieldComponent.js +1 -1
  120. package/dist/components/form/fields/MapFieldComponent.js +1 -1
  121. package/dist/components/form/fields/MultiDropdownComponent.js +1 -1
  122. package/dist/components/form/fields/MultiInputTextFieldComponent.js +1 -1
  123. package/dist/components/form/fields/NumberFieldComponent.js +1 -1
  124. package/dist/components/form/fields/PasswordFieldComponent.js +1 -1
  125. package/dist/components/form/fields/PhoneNumberComponent.js +1 -1
  126. package/dist/components/form/fields/PriceFieldComponent.js +1 -1
  127. package/dist/components/form/fields/RadioFieldComponent.js +1 -1
  128. package/dist/components/form/fields/RangeFieldComponent.js +1 -1
  129. package/dist/components/form/fields/RatingFieldComponent.js +1 -1
  130. package/dist/components/form/fields/ReferenceFieldComponent.js +1 -1
  131. package/dist/components/form/fields/RichTextComponent.js +1 -1
  132. package/dist/components/form/fields/SwitchFieldComponent.d.ts.map +1 -1
  133. package/dist/components/form/fields/SwitchFieldComponent.js +1 -1
  134. package/dist/components/form/fields/TextAreaComponent.js +1 -1
  135. package/dist/components/form/fields/TextFieldComponent.js +1 -1
  136. package/dist/components/form/fields/TimestampFieldComponent.js +1 -1
  137. package/dist/components/form/fields/index.js +1 -1
  138. package/dist/components/form/fields/internal/TiptapEditor.d.ts +4 -2
  139. package/dist/components/form/fields/internal/TiptapEditor.d.ts.map +1 -1
  140. package/dist/components/form/fields/internal/TiptapEditor.js +2 -2
  141. package/dist/components/form/internal/ImageViewerDialog.d.ts.map +1 -1
  142. package/dist/components/form/internal/ImageViewerDialog.js +1 -1
  143. package/dist/components/index.d.ts +2 -0
  144. package/dist/components/index.d.ts.map +1 -1
  145. package/dist/components/index.js +1 -1
  146. package/dist/contexts/UploadContext.js +1 -1
  147. package/dist/contexts/index.js +1 -1
  148. package/dist/fieldTypeRegistry.d.ts +3 -0
  149. package/dist/fieldTypeRegistry.d.ts.map +1 -1
  150. package/dist/fieldTypeRegistry.js +1 -1
  151. package/dist/forms/hooks/index.js +1 -1
  152. package/dist/forms/hooks/useController.js +1 -1
  153. package/dist/forms/hooks/useEntityField.js +1 -1
  154. package/dist/forms/hooks/useEntityForm.d.ts +3 -2
  155. package/dist/forms/hooks/useEntityForm.d.ts.map +1 -1
  156. package/dist/forms/hooks/useEntityForm.js +1 -1
  157. package/dist/forms/index.js +1 -1
  158. package/dist/forms/types.d.ts +0 -8
  159. package/dist/forms/types.d.ts.map +1 -1
  160. package/dist/forms/utils/buildInitialValues.js +1 -1
  161. package/dist/forms/utils/getFieldsForOperation.js +1 -1
  162. package/dist/forms/utils/index.js +1 -1
  163. package/dist/forms/utils/isFieldEditable.js +1 -1
  164. package/dist/forms/utils/translateFieldLabel.js +1 -1
  165. package/dist/forms/utils/validateEntity.js +1 -1
  166. package/dist/hooks/index.d.ts +1 -1
  167. package/dist/hooks/index.d.ts.map +1 -1
  168. package/dist/hooks/index.js +1 -1
  169. package/dist/hooks/useCrudFilters.d.ts +0 -15
  170. package/dist/hooks/useCrudFilters.d.ts.map +1 -1
  171. package/dist/hooks/useCrudFilters.js +1 -1
  172. package/dist/hooks/useEntityFavorites.js +1 -1
  173. package/dist/hooks/useFileUpload.d.ts.map +1 -1
  174. package/dist/hooks/useFileUpload.js +1 -1
  175. package/dist/hooks/useFormNavigationGuard.d.ts.map +1 -1
  176. package/dist/hooks/useFormNavigationGuard.js +1 -1
  177. package/dist/hooks/useRelatedItems.d.ts +1 -2
  178. package/dist/hooks/useRelatedItems.d.ts.map +1 -1
  179. package/dist/hooks/useRelatedItems.js +1 -1
  180. package/dist/hooks/useUnsavedChangesWarning.d.ts +3 -24
  181. package/dist/hooks/useUnsavedChangesWarning.d.ts.map +1 -1
  182. package/dist/hooks/useUnsavedChangesWarning.js +1 -1
  183. package/dist/index.d.ts +5 -3
  184. package/dist/index.d.ts.map +1 -1
  185. package/dist/index.js +1 -1
  186. package/dist/registerBuiltinFieldTypes.d.ts.map +1 -1
  187. package/dist/registerBuiltinFieldTypes.js +1 -1
  188. package/dist/stores/FormStore.d.ts.map +1 -1
  189. package/dist/stores/FormStore.js +1 -1
  190. package/dist/stores/UploadStore.d.ts.map +1 -1
  191. package/dist/stores/UploadStore.js +1 -1
  192. package/dist/stores/index.js +1 -1
  193. package/dist/tsconfig.tsbuildinfo +1 -1
  194. package/dist/types.d.ts +22 -19
  195. package/dist/types.d.ts.map +1 -1
  196. package/dist/types.js +1 -0
  197. package/dist/useBaseCrudList.d.ts +1 -4
  198. package/dist/useBaseCrudList.d.ts.map +1 -1
  199. package/dist/useBaseCrudList.js +1 -1
  200. package/dist/useCrud.d.ts +1 -2
  201. package/dist/useCrud.d.ts.map +1 -1
  202. package/dist/useCrud.js +1 -1
  203. package/dist/useCrudCardList.d.ts +1 -2
  204. package/dist/useCrudCardList.d.ts.map +1 -1
  205. package/dist/useCrudCardList.js +1 -1
  206. package/dist/useCrudList.d.ts +1 -1
  207. package/dist/useCrudList.d.ts.map +1 -1
  208. package/dist/useCrudList.js +1 -1
  209. package/dist/utils/collections.js +1 -1
  210. package/dist/utils/fileStorage.d.ts +10 -5
  211. package/dist/utils/fileStorage.d.ts.map +1 -1
  212. package/dist/utils/fileStorage.js +1 -1
  213. package/dist/utils/imageProcessing.js +1 -1
  214. package/dist/utils/imageStorage.d.ts +9 -4
  215. package/dist/utils/imageStorage.d.ts.map +1 -1
  216. package/dist/utils/imageStorage.js +1 -1
  217. package/dist/utils/imageUtils.js +1 -1
  218. package/dist/utils/mergeWithOptimistic.d.ts.map +1 -1
  219. package/dist/utils/mergeWithOptimistic.js +1 -1
  220. package/dist/utils/sanitizeHtml.d.ts +6 -0
  221. package/dist/utils/sanitizeHtml.d.ts.map +1 -0
  222. package/dist/utils/sanitizeHtml.js +1 -0
  223. package/dist/utils/scopeUtils.d.ts +1 -2
  224. package/dist/utils/scopeUtils.d.ts.map +1 -1
  225. package/dist/utils/scopeUtils.js +1 -1
  226. package/dist/utils/uploadValidation.d.ts +5 -4
  227. package/dist/utils/uploadValidation.d.ts.map +1 -1
  228. package/dist/utils/uploadValidation.js +1 -1
  229. package/package.json +11 -8
  230. package/dist/adapters/FirestoreAdapter.d.ts +0 -65
  231. package/dist/adapters/FirestoreAdapter.d.ts.map +0 -1
  232. package/dist/adapters/FirestoreAdapter.js +0 -1
  233. package/dist/components/EntityDisplayRenderer.d.ts +0 -43
  234. package/dist/components/EntityDisplayRenderer.d.ts.map +0 -1
  235. package/dist/components/EntityDisplayRenderer.js +0 -1
  236. package/dist/components/EntityFormRenderer.d.ts +0 -86
  237. package/dist/components/EntityFormRenderer.d.ts.map +0 -1
  238. package/dist/components/EntityFormRenderer.js +0 -1
package/README.md CHANGED
@@ -7,10 +7,9 @@ Complete CRUD operations and form components for DoNotDev framework.
7
7
  ```typescript
8
8
  import { useCrud, EntityFormRenderer } from '@donotdev/crud';
9
9
 
10
- // Data operations
10
+ // Data operations (requires configureProviders({ crud: adapter }) at app startup)
11
11
  function UserList() {
12
12
  const { data, loading, query } = useCrud('users', {
13
- backend: 'firestore',
14
13
  schema: UserSchema
15
14
  });
16
15
 
@@ -73,7 +72,6 @@ const {
73
72
  add,
74
73
  query,
75
74
  } = useCrud('users', {
76
- backend: 'firestore',
77
75
  schema: UserSchema,
78
76
  });
79
77
 
@@ -128,26 +126,36 @@ const UserEntity = {
128
126
 
129
127
  ## Backend Adapters
130
128
 
131
- ### FirestoreAdapter
129
+ ### FirestoreAdapter (`@donotdev/firebase`)
132
130
 
133
- **ONLY for:** `backend: 'firestore'` mode (direct client access, no functions)
131
+ Direct Firestore SDK operations with real-time subscriptions.
134
132
 
135
- Direct Firestore SDK operations with real-time subscriptions. Use for simple client-side data
136
- like user preferences or settings. Adds technical fields client-side (no server to generate them).
133
+ ```typescript
134
+ import { FirestoreAdapter } from '@donotdev/firebase';
135
+ configureProviders({ crud: new FirestoreAdapter() });
136
+ ```
137
+
138
+ **When to use:** User preferences, settings, prototyping without functions.
139
+ **When NOT to use:** Production data requiring server-side security — use `FunctionsAdapter` instead.
140
+
141
+ ### FunctionsAdapter (`@donotdev/crud`)
142
+
143
+ Callable functions adapter (Firebase Cloud Functions or Supabase Edge Functions).
137
144
 
138
- **When to use:**
139
- - User preferences, settings, client-only data
140
- - Simple data that doesn't require server-side security
141
- - Prototyping without functions
145
+ ```typescript
146
+ import { FunctionsAdapter } from '@donotdev/crud';
147
+ configureProviders({ crud: new FunctionsAdapter() });
148
+ ```
149
+
150
+ Server generates technical fields and enforces security rules. Use for production data requiring validation, auth checks, and audit logs.
142
151
 
143
- **When NOT to use:**
144
- - Production data requiring server-side security
145
- - Sensitive data needing admin checks
146
- - When functions are available (use `FunctionsAdapter` instead)
152
+ ### SupabaseCrudAdapter (`@donotdev/supabase`)
147
153
 
148
- ### FunctionsAdapter
154
+ Direct PostgREST access with RLS-based security.
149
155
 
150
- **ONLY for:** `backend: 'functions'` mode (server-side security)
156
+ ```typescript
157
+ import { SupabaseCrudAdapter } from '@donotdev/supabase';
158
+ configureProviders({ crud: new SupabaseCrudAdapter(supabaseClient) });
159
+ ```
151
160
 
152
- Firebase Functions HTTP endpoints for CRUD operations. Server generates technical fields and
153
- enforces security rules. Use for production data requiring validation, auth checks, and audit logs.
161
+ DB-level security via Row Level Security and column grants. Field name mapping (camelCase ↔ snake_case) handled at adapter boundary.
@@ -1,8 +1,6 @@
1
- import type { dndevSchema, QueryClient } from '@donotdev/core';
2
- import type { BackendType, CrudStoreApi, CrudServiceInterface, CacheOptions, MutationOptions } from './types';
3
- import type { QueryOptions } from './adapters/FirestoreAdapter';
4
- import type { PaginatedQueryResult } from './adapters/FunctionsAdapter';
5
- export type { QueryOptions, CacheOptions };
1
+ import type { dndevSchema, QueryClient, QueryOptions, PaginatedQueryResult, ListSchemaType, SecurityContext } from '@donotdev/core';
2
+ import type { CrudStoreApi, CrudServiceInterface, CacheOptions, MutationOptions } from './types';
3
+ export type { CacheOptions };
6
4
  /**
7
5
  * Complete CRUD orchestrator with TanStack Query caching
8
6
  *
@@ -12,8 +10,15 @@ export type { QueryOptions, CacheOptions };
12
10
  */
13
11
  declare class CrudService implements CrudServiceInterface {
14
12
  private adapter;
15
- private backend;
16
13
  private store;
14
+ /** Optional security context (SOC2 audit logging, rate limiting, PII encryption) */
15
+ private security;
16
+ /**
17
+ * Current authenticated user ID — used as rate limit key prefix and audit actor.
18
+ * Call `setCurrentUser(userId)` after auth state changes so CRUD operations are
19
+ * scoped per-user rather than per-collection.
20
+ */
21
+ private currentUserId;
17
22
  /**
18
23
  * Determine if success toast should be shown based on per-call options and global setting
19
24
  * - options.showSuccessToast=true → always show
@@ -22,7 +27,64 @@ declare class CrudService implements CrudServiceInterface {
22
27
  */
23
28
  private _shouldShowSuccessToast;
24
29
  setStore(store: CrudStoreApi): void;
25
- initialize(backend: BackendType): Promise<void>;
30
+ /**
31
+ * Inject a security context for SOC2-compliant audit logging, rate limiting, and PII encryption.
32
+ * Call at app startup after configureProviders():
33
+ * ```typescript
34
+ * getCrudService().setSecurity(security);
35
+ * ```
36
+ *
37
+ * @version 0.0.1
38
+ * @since 0.0.1
39
+ */
40
+ setSecurity(security: SecurityContext): void;
41
+ /**
42
+ * Set the current authenticated user ID so rate limiting and audit logs are
43
+ * scoped per-user rather than per-collection. Call after auth state changes.
44
+ *
45
+ * ```typescript
46
+ * onAuthStateChanged((user) => getCrudService().setCurrentUser(user?.id ?? null));
47
+ * ```
48
+ *
49
+ * @version 0.0.1
50
+ * @since 0.0.1
51
+ */
52
+ setCurrentUser(userId: string | null): void;
53
+ /**
54
+ * Resolve PII fields for a collection from the schema metadata (if available).
55
+ * Returns empty array when no security config or piiFields defined.
56
+ */
57
+ private _getPiiFields;
58
+ /**
59
+ * Emit a CRUD audit event when security context is present.
60
+ * Skips silently if auditLog is explicitly false on the schema.
61
+ */
62
+ private _auditCrud;
63
+ /**
64
+ * Build the rate limit key for a write/read operation.
65
+ * Uses `<userId>:<collection>` when a current user is set, ensuring per-user
66
+ * isolation.
67
+ *
68
+ * **Warning:** Falls back to `<collection>` when no user is set, which means all
69
+ * unauthenticated callers share one rate-limit bucket per collection. A single
70
+ * anonymous client can exhaust the limit for all others. Call `setCurrentUser()`
71
+ * after auth state changes to enable per-user isolation.
72
+ */
73
+ private _rateLimitKey;
74
+ /** Ensure adapter is initialized from provider registry. */
75
+ private _ensureAdapter;
76
+ /**
77
+ * Throws if the active adapter is direct (not server-side) and the entity schema
78
+ * has any field with restricted visibility. Call before any adapter read or write.
79
+ */
80
+ private _assertCanAccess;
81
+ /**
82
+ * Initialize the CRUD adapter from the provider registry.
83
+ * Apps must call `configureProviders({ crud: adapter })` at startup before any CRUD.
84
+ *
85
+ * @throws If no CRUD provider is configured
86
+ */
87
+ initialize(): Promise<void>;
26
88
  /**
27
89
  * Get shared QueryClient instance from @donotdev/core.
28
90
  * Uses singleton on client, new instance per request on server (SSR-safe).
@@ -33,8 +95,8 @@ declare class CrudService implements CrudServiceInterface {
33
95
  * Returns reactive loading states via TanStack Query.
34
96
  * @param schema - Accepts dndevSchema<unknown> since OperationSchemas uses unknown. T is for return type.
35
97
  */
36
- getListQueryOptions<T>(collection: string, queryOptions: QueryOptions, schema: dndevSchema<unknown>, cacheOptions?: CacheOptions, schemaType?: 'list' | 'listCard'): {
37
- queryKey: readonly ["crud", string, "query", "list" | "listCard", string];
98
+ getListQueryOptions<T>(collection: string, queryOptions: QueryOptions, schema: dndevSchema<unknown>, cacheOptions?: CacheOptions, schemaType?: ListSchemaType): {
99
+ queryKey: readonly ["crud", string, "query", string];
38
100
  queryFn: () => Promise<PaginatedQueryResult<T>>;
39
101
  staleTime: number;
40
102
  };
@@ -58,6 +120,13 @@ declare class CrudService implements CrudServiceInterface {
58
120
  * Automatically syncs both list and document caches for consistency.
59
121
  */
60
122
  private _updateListCaches;
123
+ /**
124
+ * Check uniqueness constraints against cached data (list/listCard queries).
125
+ * For findOrCreate: returns existing item's ID without server call.
126
+ * For strict uniqueness: throws error to block the operation.
127
+ * Returns null if no match found or no cache available.
128
+ */
129
+ private _checkUniquenessFromCache;
61
130
  /**
62
131
  * Get an item from any list cache for this collection.
63
132
  * Used to get previous data for optimistic operations when individual GET cache is empty.
@@ -70,7 +139,7 @@ declare class CrudService implements CrudServiceInterface {
70
139
  get<T>(collection: string, id: string, schema: dndevSchema<T>, options?: CacheOptions): Promise<T | null>;
71
140
  /** Internal: fetch from adapter with store updates */
72
141
  private _getFromAdapter;
73
- query<T>(collection: string, options: QueryOptions, schema: dndevSchema<T>, cacheOptions?: CacheOptions, schemaType?: 'list' | 'listCard'): Promise<PaginatedQueryResult<T>>;
142
+ query<T>(collection: string, options: QueryOptions, schema: dndevSchema<T>, cacheOptions?: CacheOptions, schemaType?: ListSchemaType): Promise<PaginatedQueryResult<T>>;
74
143
  /** Internal: query from adapter with store updates */
75
144
  private _queryFromAdapter;
76
145
  /**
@@ -80,11 +149,15 @@ declare class CrudService implements CrudServiceInterface {
80
149
  */
81
150
  private _getEntityName;
82
151
  set<T>(collection: string, id: string, data: T, schema: dndevSchema<T>, options?: MutationOptions): Promise<void>;
83
- update<T>(collection: string, id: string, data: Partial<T>, options?: MutationOptions): Promise<void>;
152
+ update<T>(collection: string, id: string, data: Partial<T>, schema?: dndevSchema<T>, options?: MutationOptions): Promise<void>;
84
153
  delete(collection: string, id: string, options?: MutationOptions): Promise<void>;
85
154
  add<T>(collection: string, data: T, schema: dndevSchema<T>, options?: MutationOptions): Promise<string>;
86
155
  subscribe<T>(collection: string, id: string, callback: (data: T | null, error?: Error) => void, schema: dndevSchema<T>): () => void;
156
+ /** Synchronous subscribe — only called once adapter is confirmed ready. */
157
+ private _subscribeSync;
87
158
  subscribeToCollection<T>(collection: string, options: QueryOptions, callback: (data: T[], error?: Error) => void, schema: dndevSchema<T>): () => void;
159
+ /** Synchronous subscribeToCollection — only called once adapter is confirmed ready. */
160
+ private _subscribeToCollectionSync;
88
161
  addOptimistic<T extends {
89
162
  id?: string;
90
163
  }>(collection: string, data: T, schema: dndevSchema<T>, options?: MutationOptions): Promise<T & {
@@ -1 +1 @@
1
- {"version":3,"file":"CrudService.d.ts","sourceRoot":"","sources":["../src/CrudService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CrudService.d.ts","sourceRoot":"","sources":["../src/CrudService.ts"],"names":[],"mappings":"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"}
@@ -1 +1,9 @@
1
- import{toast as p}from"@donotdev/components";import{createSingleton as m,handleError as d,getQueryClient as w,getI18nInstance as f}from"@donotdev/core";import{FirestoreAdapter as S}from"./adapters/FirestoreAdapter";import{FunctionsAdapter as C}from"./adapters/FunctionsAdapter";const g=1/0,Q=1e3*60*30;class c{adapter=null;backend=null;store=null;_shouldShowSuccessToast(t){return t?.showSuccessToast===!0?!0:t?.showSuccessToast===!1?!1:!this.store?.getState().hideSuccessToasts}setStore(t){this.store=t}async initialize(t){if(!(this.backend===t&&this.adapter)){if(this.backend=t,t==="functions")this.adapter=new C;else if(t==="firestore")this.adapter=new S;else throw new Error(`Unknown backend: ${t}`);this.store&&(this.store.getState().setBackend(t),this.store.getState().setCrudService(this))}}getQueryClient(){return w()}getListQueryOptions(t,e,i,h,s="list"){const r=this.adapter,a=()=>this.initialize("firestore");return{queryKey:["crud",t,"query",s,JSON.stringify(e)],queryFn:async()=>{if(r||await a(),!this.adapter)throw new Error("Adapter not initialized");return this.adapter.query(t,e,i,s)},staleTime:h?.staleTime??g}}getDocQueryOptions(t,e,i,h){const s=this.adapter,r=()=>this.initialize("firestore");return{queryKey:["crud",t,"get",e],queryFn:async()=>{if(s||await r(),!this.adapter)throw new Error("Adapter not initialized");return this.adapter.get(t,e,i)},staleTime:h?.staleTime??g}}_updateGetCache(t,e,i,h){const s=this.getQueryClient(),r=["crud",t,"get",e];h==="delete"?s.removeQueries({queryKey:r}):(h==="update"||h==="add")&&i&&s.setQueryData(r,a=>a?{...a,...i}:i)}_updateListCaches(t,e,i,h){const s=this.getQueryClient();this._updateGetCache(t,e,i,h),s.setQueriesData({queryKey:["crud",t]},r=>{if(r&&typeof r=="object"&&"items"in r){const a=r;let u=a.items;return h==="delete"?(u=a.items.filter(n=>n.id!==e),{...a,items:u,total:(a.total??u.length)-1}):h==="add"&&i?(u=[...a.items,{...i,id:e}],{...a,items:u,total:(a.total??a.items.length)+1}):h==="update"&&i&&a.items.some(o=>o.id===e)?(u=a.items.map(o=>o.id===e?{...o,...i}:o),{...a,items:u}):r}if(Array.isArray(r)){if(h==="delete")return r.filter(a=>a.id!==e);if(h==="add"&&i)return[...r,{...i,id:e}];if(h==="update"&&i)return r.some(u=>u.id===e)?r.map(u=>u.id===e?{...u,...i}:u):r}return r})}_getItemFromListCache(t,e){const h=this.getQueryClient().getQueriesData({queryKey:["crud",t]});for(const[,s]of h)if(s?.items){const r=s.items.find(a=>a.id===e);if(r)return r}return null}async invalidateCollection(t){await this.getQueryClient().invalidateQueries({queryKey:["crud",t]})}async get(t,e,i,h){if(this.adapter||await this.initialize("firestore"),h?.noCache)return this._getFromAdapter(t,e,i);this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{return await this.getQueryClient().fetchQuery({queryKey:["crud",t,"get",e],queryFn:()=>this._getFromAdapter(t,e,i,!1),staleTime:h?.staleTime??g})}catch(s){const r=d(s,{userMessage:`Failed to fetch ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,r),r}finally{this.store&&this.store.getState().setLoading(t,!1)}}async _getFromAdapter(t,e,i,h=!0){this.store&&h&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{if(!this.adapter)throw new Error("Adapter not initialized");return await this.adapter.get(t,e,i)}catch(s){const r=d(s,{userMessage:`Failed to fetch ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,r),r}finally{this.store&&h&&this.store.getState().setLoading(t,!1)}}async query(t,e,i,h,s="list"){if(this.adapter||await this.initialize("firestore"),h?.noCache)return this._queryFromAdapter(t,e,i,!0,s);this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{return await this.getQueryClient().fetchQuery({queryKey:["crud",t,"query",s,JSON.stringify(e)],queryFn:()=>this._queryFromAdapter(t,e,i,!1,s),staleTime:h?.staleTime??g})}catch(r){const a=d(r,{userMessage:`Failed to query ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,a),a}finally{this.store&&this.store.getState().setLoading(t,!1)}}async _queryFromAdapter(t,e,i,h=!0,s="list"){this.store&&h&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{if(!this.adapter)throw new Error("Adapter not initialized");return await this.adapter.query(t,e,i,s)}catch(r){const a=d(r,{userMessage:`Failed to query ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,a),a}finally{this.store&&h&&this.store.getState().setLoading(t,!1)}}_getEntityName(t){const e=f();let i=`entity-${t}`,h=e.t("name",{ns:i,defaultValue:null});return h&&h!=="name"&&h!==`${i}:name`||t.endsWith("s")&&t.length>1&&(i=`entity-${t.slice(0,-1)}`,h=e.t("name",{ns:i,defaultValue:null}),h&&h!=="name"&&h!==`${i}:name`)?h:t}async set(t,e,i,h,s){this.adapter||await this.initialize("firestore"),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const a=this.getQueryClient().getQueryData(["crud",t,"get",e])??null;this.store&&this.store.getState().updateOptimistic(t,e,i,a);try{if(!this.adapter)throw new Error("Adapter not initialized");if(await this.adapter.set(t,e,i,h),this._updateListCaches(t,e,i,"update"),this.store&&this.store.getState().confirmUpdate(t,e),this._shouldShowSuccessToast(s)){const u=f(),n=this._getEntityName(t);p("success",u.t("messages.updateSuccess",{ns:"crud",entity:n}))}}catch(u){a&&this._updateListCaches(t,e,a,"update"),this.store&&this.store.getState().rejectUpdate(t,e);const n=d(u,{userMessage:`Failed to save ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,n),n}finally{this.store&&this.store.getState().setLoading(t,!1)}}async update(t,e,i,h){this.adapter||await this.initialize("firestore"),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const r=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e),a=r?{...r,...i}:{...i,id:e};this.store&&r&&this.store.getState().updateOptimistic(t,e,a,r);try{if(!this.adapter)throw new Error("Adapter not initialized");if(await this.adapter.update(t,e,i),this._updateListCaches(t,e,a,"update"),this.store&&this.store.getState().confirmUpdate(t,e),this._shouldShowSuccessToast(h)){const u=f(),n=this._getEntityName(t);p("success",u.t("messages.updateSuccess",{ns:"crud",entity:n}))}}catch(u){r&&this._updateListCaches(t,e,r,"update"),this.store&&this.store.getState().rejectUpdate(t,e);const n=d(u,{userMessage:`Failed to update ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,n),n}finally{this.store&&this.store.getState().setLoading(t,!1)}}async delete(t,e,i){this.adapter||await this.initialize("firestore"),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const s=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e);this.store&&s&&this.store.getState().deleteOptimistic(t,e,s);try{if(!this.adapter)throw new Error("Adapter not initialized");if(await this.adapter.delete(t,e),this._updateListCaches(t,e,null,"delete"),this.store&&this.store.getState().confirmDelete(t,e),this._shouldShowSuccessToast(i)){const r=f(),a=this._getEntityName(t);p("success",r.t("messages.deleteSuccess",{ns:"crud",entity:a}))}}catch(r){s&&this._updateListCaches(t,e,s,"add"),this.store&&this.store.getState().rejectDelete(t,e);const a=d(r,{userMessage:`Failed to delete ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,a),a}finally{this.store&&this.store.getState().setLoading(t,!1)}}async add(t,e,i,h){this.adapter||await this.initialize("firestore"),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const s=`temp_${crypto.randomUUID()}`;this.store&&this.store.getState().addOptimistic(t,s,{...e,id:s});try{if(!this.adapter)throw new Error("Adapter not initialized");const r=await this.adapter.add(t,e,i);if(this._updateListCaches(t,s,null,"delete"),this._updateListCaches(t,r,{...e,id:r},"add"),this.store&&this.store.getState().confirmOptimistic(t,s,r,{...e,id:r}),this._shouldShowSuccessToast(h)){const a=f(),u=this._getEntityName(t);p("success",a.t("messages.createSuccess",{ns:"crud",entity:u}))}return r}catch(r){this._updateListCaches(t,s,null,"delete"),this.store&&this.store.getState().rejectOptimistic(t,s);const a=d(r,{userMessage:`Failed to create ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,a),a}finally{this.store&&this.store.getState().setLoading(t,!1)}}subscribe(t,e,i,h){this.adapter||this.initialize("firestore").catch(()=>{});try{return this.adapter?this.adapter.subscribe(t,e,(s,r)=>{s&&this.getQueryClient().setQueryData(["crud",t,"get",e],s),i(s,r)},h):(i(null,new Error("Adapter not initialized")),()=>{})}catch(s){return i(null,d(s)),()=>{}}}subscribeToCollection(t,e,i,h){this.adapter||this.initialize("firestore").catch(()=>{});try{return this.adapter?this.adapter.subscribeToCollection(t,e,(s,r)=>{s&&this.getQueryClient().setQueryData(["crud",t,"query",JSON.stringify(e)],{items:s}),i(s,r)},h):(i([],new Error("Adapter not initialized")),()=>{})}catch(s){return i([],d(s)),()=>{}}}async addOptimistic(t,e,i,h){this.adapter||await this.initialize("firestore");const s=`temp_${crypto.randomUUID()}`,r={...e,id:s,_optimistic:!0};this.store&&this.store.getState().addOptimistic(t,s,r);try{if(!this.adapter)throw new Error("Adapter not initialized");const a=await this.adapter.add(t,e,i),u={...e,id:a};if(this._updateListCaches(t,s,null,"delete"),this._updateListCaches(t,a,u,"add"),this.store&&this.store.getState().confirmOptimistic(t,s,a,u),this._shouldShowSuccessToast(h)){const n=f(),o=this._getEntityName(t);p("success",n.t("messages.createSuccess",{ns:"crud",entity:o}))}return u}catch(a){throw this._updateListCaches(t,s,null,"delete"),this.store&&this.store.getState().rejectOptimistic(t,s),d(a,{userMessage:`Failed to create ${t}`,showNotification:!0})}}async updateOptimistic(t,e,i,h,s){this.adapter||await this.initialize("firestore");const a=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e),u=a?{...a,...i,_optimistic:!0}:{...i,id:e,_optimistic:!0};this.store&&a&&this.store.getState().updateOptimistic(t,e,u,a);try{if(!this.adapter)throw new Error("Adapter not initialized");await this.adapter.update(t,e,i);const n={...u,_optimistic:void 0};if(this._updateListCaches(t,e,n,"update"),this.store&&this.store.getState().confirmUpdate(t,e),this._shouldShowSuccessToast(s)){const o=f(),y=this._getEntityName(t);p("success",o.t("messages.updateSuccess",{ns:"crud",entity:y}))}return n}catch(n){throw a&&this._updateListCaches(t,e,a,"update"),this.store&&this.store.getState().rejectUpdate(t,e),d(n,{userMessage:`Failed to update ${t}`,showNotification:!0})}}async deleteOptimistic(t,e,i){this.adapter||await this.initialize("firestore");const s=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e);this.store&&s&&this.store.getState().deleteOptimistic(t,e,s);try{if(!this.adapter)throw new Error("Adapter not initialized");if(await this.adapter.delete(t,e),this._updateListCaches(t,e,null,"delete"),this.store&&this.store.getState().confirmDelete(t,e),this._shouldShowSuccessToast(i)){const r=f(),a=this._getEntityName(t);p("success",r.t("messages.deleteSuccess",{ns:"crud",entity:a}))}}catch(r){throw s&&this._updateListCaches(t,e,s,"add"),this.store&&this.store.getState().rejectDelete(t,e),d(r,{userMessage:`Failed to delete ${t}`,showNotification:!0})}}}const N=m(()=>new c);export{N as getCrudService};
1
+ import{toast as p}from"@donotdev/components";import{createSingleton as w,handleError as d,getQueryClient as _,getI18nInstance as y,hasProvider as E,getProvider as C,hasRestrictedVisibility as A,validateWithSchema as L}from"@donotdev/core";import{LIST_SCHEMA_TYPE as f}from"@donotdev/core";import{CRUD_OPERATION as h}from"./types";const m=1/0,q=1e3*60*30;class T{adapter=null;store=null;security=null;currentUserId=null;_shouldShowSuccessToast(t){return t?.showSuccessToast===!0?!0:t?.showSuccessToast===!1?!1:!this.store?.getState().hideSuccessToasts}setStore(t){this.store=t}setSecurity(t){this.security=t}setCurrentUser(t){this.currentUserId=t}_getPiiFields(t){return t?.security?.piiFields??[]}_auditCrud(t,e,i,r){this.security&&this.security.audit({type:t,collection:e,docId:i,userId:r??this.currentUserId??void 0})}_rateLimitKey(t){return this.currentUserId?`${this.currentUserId}:${t}`:t}async _ensureAdapter(){this.adapter||await this.initialize()}_assertCanAccess(t,e){if(this.adapter&&e&&A(e)&&!this.adapter.serverSideOnly&&!this.adapter.dbLevelSecurity)throw new Error(`[dndev] Direct adapter cannot access "${t}": entity has restricted field visibility. Use a server-side adapter (FunctionsAdapter for Firebase, or SupabaseCrudAdapter with RLS enabled) to enforce field-level security (serverSideOnly = true | dbLevelSecurity = true).`)}async initialize(){if(!this.adapter){if(!E("crud"))throw new Error(`[dndev] No CRUD adapter configured. Call configureProviders({ crud: yourAdapter }) at app startup.
2
+ Examples:
3
+ import { FirestoreAdapter } from "@donotdev/firebase";
4
+ import { FunctionsAdapter } from "@donotdev/crud";
5
+ import { SupabaseCrudAdapter } from "@donotdev/supabase";
6
+
7
+ configureProviders({ crud: new FirestoreAdapter() }) // Firebase direct
8
+ configureProviders({ crud: new FunctionsAdapter() }) // via callable functions
9
+ configureProviders({ crud: new SupabaseCrudAdapter(sb) }) // Supabase direct (RLS)`);this.adapter=C("crud"),this.store&&this.store.getState().setCrudService(this)}}getQueryClient(){return _()}getListQueryOptions(t,e,i,r,o=f.LIST){const s=this.adapter,a=()=>this._ensureAdapter();return{queryKey:["crud",t,"query",JSON.stringify(e)],queryFn:async()=>{if(s||await a(),!this.adapter)throw new Error("Adapter not initialized");return this.adapter.query(t,e,i,o)},staleTime:r?.staleTime??m}}getDocQueryOptions(t,e,i,r){const o=this.adapter,s=()=>this._ensureAdapter();return{queryKey:["crud",t,"get",e],queryFn:async()=>{if(o||await s(),!this.adapter)throw new Error("Adapter not initialized");return this.adapter.get(t,e,i)},staleTime:r?.staleTime??m}}_updateGetCache(t,e,i,r){const o=this.getQueryClient(),s=["crud",t,"get",e];r===h.DELETE?o.removeQueries({queryKey:s}):r===h.SET&&i?o.setQueryData(s,i):(r===h.UPDATE||r===h.ADD)&&i&&o.setQueryData(s,a=>a?{...a,...i}:i)}_updateListCaches(t,e,i,r){const o=this.getQueryClient();this._updateGetCache(t,e,i,r),o.setQueriesData({queryKey:["crud",t]},s=>{if(s&&typeof s=="object"&&"items"in s){const a=s;let c=a.items;return r===h.DELETE?(c=a.items.filter(n=>n!=null&&typeof n=="object"&&n.id!==e),{...a,items:c,total:Math.max(0,(a.total??c.length)-1)}):r===h.ADD&&i?(c=[...a.items,{...i,id:e}],{...a,items:c,total:(a.total??a.items.length)+1}):(r===h.UPDATE||r===h.SET)&&i&&a.items.some(n=>n!=null&&typeof n=="object"&&n.id===e)?(c=a.items.map(n=>n==null||typeof n!="object"||n.id!==e?n:r===h.SET?{...i,id:e}:{...n,...i}),{...a,items:c}):s}if(Array.isArray(s)){if(r===h.DELETE)return s.filter(a=>a.id!==e);if(r===h.ADD&&i)return[...s,{...i,id:e}];if((r===h.UPDATE||r===h.SET)&&i)return s.some(a=>a!=null&&typeof a=="object"&&a.id===e)?s.map(a=>a==null||typeof a!="object"||a.id!==e?a:r===h.SET?{...i,id:e}:{...a,...i}):s}return s})}_checkUniquenessFromCache(t,e,i){const r=i.metadata?.uniqueKeys;if(!r||r.length===0)return null;const o=this.getQueryClient().getQueriesData({queryKey:["crud",t]}),s=[];for(const[,c]of o)c&&typeof c=="object"&&"items"in c?s.push(...c.items):Array.isArray(c)&&s.push(...c);if(s.length===0)return null;const a=e;for(const c of r){if(!c.fields.every(u=>a[u]!=null&&a[u]!==""))continue;const n=s.find(u=>c.fields.every(l=>{const g=typeof a[l]=="string"?a[l].toLowerCase():a[l],S=typeof u[l]=="string"?u[l].toLowerCase():u[l];return g===S}));if(n){if(c.findOrCreate)return n.id;const u=c.fields.join(" + ");throw d(new Error(c.errorMessage||`Duplicate ${u}`),{userMessage:c.errorMessage||`A record with this ${u} already exists`,showNotification:!0})}}return null}_getItemFromListCache(t,e){const i=this.getQueryClient().getQueriesData({queryKey:["crud",t]});for(const[,r]of i)if(r?.items){const o=r.items.find(s=>s.id===e);if(o)return o}return null}async invalidateCollection(t){await this.getQueryClient().invalidateQueries({queryKey:["crud",t]})}async get(t,e,i,r){if(await this._ensureAdapter(),r?.noCache)return this._getFromAdapter(t,e,i);this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{return await this.getQueryClient().fetchQuery({queryKey:["crud",t,"get",e],queryFn:()=>this._getFromAdapter(t,e,i,!1),staleTime:r?.staleTime??m})}catch(o){const s=d(o,{userMessage:`Failed to fetch ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,s),s}finally{this.store&&this.store.getState().setLoading(t,!1)}}async _getFromAdapter(t,e,i,r=!0){this.store&&r&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{if(!this.adapter)throw new Error("Adapter not initialized");return this._assertCanAccess(t,i),await this.adapter.get(t,e,i)}catch(o){const s=d(o,{userMessage:`Failed to fetch ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,s),s}finally{this.store&&r&&this.store.getState().setLoading(t,!1)}}async query(t,e,i,r,o=f.LIST){if(await this._ensureAdapter(),r?.noCache)return this._queryFromAdapter(t,e,i,!0,o);this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{return await this.getQueryClient().fetchQuery({queryKey:["crud",t,"query",JSON.stringify(e)],queryFn:()=>this._queryFromAdapter(t,e,i,!1,o),staleTime:r?.staleTime??m})}catch(s){const a=d(s,{userMessage:`Failed to query ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,a),a}finally{this.store&&this.store.getState().setLoading(t,!1)}}async _queryFromAdapter(t,e,i,r=!0,o=f.LIST){this.store&&r&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));try{if(!this.adapter)throw new Error("Adapter not initialized");return this._assertCanAccess(t,i),await this.adapter.query(t,e,i,o)}catch(s){const a=d(s,{userMessage:`Failed to query ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,a),a}finally{this.store&&r&&this.store.getState().setLoading(t,!1)}}_getEntityName(t){const e=y();let i=`entity-${t}`,r=e.t("name",{ns:i,defaultValue:null});return r&&r!=="name"&&r!==`${i}:name`||t.endsWith("s")&&t.length>1&&(i=`entity-${t.slice(0,-1)}`,r=e.t("name",{ns:i,defaultValue:null}),r&&r!=="name"&&r!==`${i}:name`)?r:t}async set(t,e,i,r,o){await this._ensureAdapter(),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const s=this.getQueryClient().getQueryData(["crud",t,"get",e])??null;this.store&&this.store.getState().updateOptimistic(t,e,i,s);try{if(!this.adapter)throw new Error("Adapter not initialized");this._assertCanAccess(t,r),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const a=this._getPiiFields(r),c=a.length>0&&this.security?this.security.encryptPii(i,a):i;if(await this.adapter.set(t,e,c,r),this._updateListCaches(t,e,i,h.SET),this.store&&this.store.getState().confirmUpdate(t,e),this._auditCrud("crud.update",t,e),this._shouldShowSuccessToast(o)){const n=y(),u=this._getEntityName(t);p("success",n.t("messages.updateSuccess",{ns:"crud",entity:u}))}}catch(a){s&&this._updateListCaches(t,e,s,h.SET),this.store&&this.store.getState().rejectUpdate(t,e);const c=d(a,{userMessage:`Failed to save ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,c),c}finally{this.store&&this.store.getState().setLoading(t,!1)}}async update(t,e,i,r,o){await this._ensureAdapter(),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const s=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e),a=s?{...s,...i}:{...i,id:e};this.store&&this.store.getState().updateOptimistic(t,e,a,s??null);try{if(!this.adapter)throw new Error("Adapter not initialized");r&&(this._assertCanAccess(t,r),L(r,i,`CrudService.update(${t})`)),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const c=r?this._getPiiFields(r):[],n=c.length>0&&this.security?this.security.encryptPii(i,c):i;if(await this.adapter.update(t,e,n),this._updateListCaches(t,e,a,h.UPDATE),this.store&&this.store.getState().confirmUpdate(t,e),this._auditCrud("crud.update",t,e),this._shouldShowSuccessToast(o)){const u=y(),l=this._getEntityName(t);p("success",u.t("messages.updateSuccess",{ns:"crud",entity:l}))}}catch(c){s?this._updateListCaches(t,e,s,h.UPDATE):this.invalidateCollection(t),this.store&&this.store.getState().rejectUpdate(t,e);const n=d(c,{userMessage:`Failed to update ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,n),n}finally{this.store&&this.store.getState().setLoading(t,!1)}}async delete(t,e,i){await this._ensureAdapter(),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const r=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e);this.store&&r&&this.store.getState().deleteOptimistic(t,e,r);try{if(!this.adapter)throw new Error("Adapter not initialized");if(this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write"),await this.adapter.delete(t,e),this._updateListCaches(t,e,null,h.DELETE),this.store&&this.store.getState().confirmDelete(t,e),this._auditCrud("crud.delete",t,e),this.security?.recordAnomaly?.("bulk.deletes",this.currentUserId??void 0),this._shouldShowSuccessToast(i)){const o=y(),s=this._getEntityName(t);p("success",o.t("messages.deleteSuccess",{ns:"crud",entity:s}))}}catch(o){r&&this._updateListCaches(t,e,r,h.ADD),this.store&&this.store.getState().rejectDelete(t,e);const s=d(o,{userMessage:`Failed to delete ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,s),s}finally{this.store&&this.store.getState().setLoading(t,!1)}}async add(t,e,i,r){await this._ensureAdapter();const o=this._checkUniquenessFromCache(t,e,i);if(o)return o;this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const s=`temp_${crypto.randomUUID()}`;this.store&&this.store.getState().addOptimistic(t,s,{...e,id:s});try{if(!this.adapter)throw new Error("Adapter not initialized");this._assertCanAccess(t,i),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const a=this._getPiiFields(i),c=a.length>0&&this.security?this.security.encryptPii(e,a):e,n=await this.adapter.add(t,c,i),u=n.id,l=n.data;if(this._updateListCaches(t,s,null,h.DELETE),this._updateListCaches(t,u,l,h.ADD),this.store&&this.store.getState().confirmOptimistic(t,s,u,l),this._auditCrud("crud.create",t,u),this._shouldShowSuccessToast(r)){const g=y(),S=this._getEntityName(t);p("success",g.t("messages.createSuccess",{ns:"crud",entity:S}))}return u}catch(a){this._updateListCaches(t,s,null,h.DELETE),this.store&&this.store.getState().rejectOptimistic(t,s);const c=d(a,{userMessage:`Failed to create ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,c),c}finally{this.store&&this.store.getState().setLoading(t,!1)}}subscribe(t,e,i,r){if(!this.adapter){let o=!1,s=null;return this._ensureAdapter().then(()=>{o||(s=this._subscribeSync(t,e,i,r))}).catch(a=>{o||i(null,d(a))}),()=>{o=!0,s?.()}}return this._subscribeSync(t,e,i,r)}_subscribeSync(t,e,i,r){try{const o=this.adapter;return o?o.subscribe?(this._assertCanAccess(t,r),o.subscribe(t,e,(s,a)=>{s&&this.getQueryClient().setQueryData(["crud",t,"get",e],s),i(s,a)},r)):()=>{}:(i(null,new Error("Adapter not initialized")),()=>{})}catch(o){return i(null,d(o)),()=>{}}}subscribeToCollection(t,e,i,r){if(!this.adapter){let o=!1,s=null;return this._ensureAdapter().then(()=>{o||(s=this._subscribeToCollectionSync(t,e,i,r))}).catch(a=>{o||i([],d(a))}),()=>{o=!0,s?.()}}return this._subscribeToCollectionSync(t,e,i,r)}_subscribeToCollectionSync(t,e,i,r){try{const o=this.adapter;return o?o.subscribeToCollection?(this._assertCanAccess(t,r),o.subscribeToCollection(t,e,(s,a)=>{s&&this.getQueryClient().setQueryData(["crud",t,"query",JSON.stringify(e)],c=>c?{...c,items:s}:{items:s}),i(s,a)},r)):()=>{}:(i([],new Error("Adapter not initialized")),()=>{})}catch(o){return i([],d(o)),()=>{}}}async addOptimistic(t,e,i,r){await this._ensureAdapter();const o=this._checkUniquenessFromCache(t,e,i);if(o)return{...e,id:o};this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const s=`temp_${crypto.randomUUID()}`,a={...e,id:s,_optimistic:!0};this.store&&this.store.getState().addOptimistic(t,s,a);try{if(!this.adapter)throw new Error("Adapter not initialized");const c=await this.adapter.add(t,e,i),n=c.id,u=c.data;if(this._updateListCaches(t,s,null,h.DELETE),this._updateListCaches(t,n,u,h.ADD),this.store&&this.store.getState().confirmOptimistic(t,s,n,u),this._auditCrud("crud.create",t,n),this._shouldShowSuccessToast(r)){const l=y(),g=this._getEntityName(t);p("success",l.t("messages.createSuccess",{ns:"crud",entity:g}))}return u}catch(c){throw this._updateListCaches(t,s,null,h.DELETE),this.store&&this.store.getState().rejectOptimistic(t,s),d(c,{userMessage:`Failed to create ${t}`,showNotification:!0})}}async updateOptimistic(t,e,i,r,o){await this._ensureAdapter(),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const s=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e),a=s?{...s,...i,_optimistic:!0}:{...i,id:e,_optimistic:!0};this.store&&s&&this.store.getState().updateOptimistic(t,e,a,s);try{if(!this.adapter)throw new Error("Adapter not initialized");await this.adapter.update(t,e,i);const{_optimistic:c,...n}=a;if(this._updateListCaches(t,e,n,h.UPDATE),this.store&&this.store.getState().confirmUpdate(t,e),this._auditCrud("crud.update",t,e),this._shouldShowSuccessToast(o)){const u=y(),l=this._getEntityName(t);p("success",u.t("messages.updateSuccess",{ns:"crud",entity:l}))}return n}catch(c){throw s&&this._updateListCaches(t,e,s,h.UPDATE),this.store&&this.store.getState().rejectUpdate(t,e),d(c,{userMessage:`Failed to update ${t}`,showNotification:!0})}}async deleteOptimistic(t,e,i){await this._ensureAdapter(),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const r=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e);this.store&&r&&this.store.getState().deleteOptimistic(t,e,r);try{if(!this.adapter)throw new Error("Adapter not initialized");if(await this.adapter.delete(t,e),this._updateListCaches(t,e,null,h.DELETE),this.store&&this.store.getState().confirmDelete(t,e),this._auditCrud("crud.delete",t,e),this.security?.recordAnomaly?.("bulk.deletes",this.currentUserId??void 0),this._shouldShowSuccessToast(i)){const o=y(),s=this._getEntityName(t);p("success",o.t("messages.deleteSuccess",{ns:"crud",entity:s}))}}catch(o){throw r&&this._updateListCaches(t,e,r,h.ADD),this.store&&this.store.getState().rejectDelete(t,e),d(o,{userMessage:`Failed to delete ${t}`,showNotification:!0})}}}const D=w(()=>new T);export{D as getCrudService};
@@ -1 +1 @@
1
- {"version":3,"file":"CrudStore.d.ts","sourceRoot":"","sources":["../src/CrudStore.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAGV,SAAS,EACT,WAAW,EAIZ,MAAM,SAAS,CAAC;AA0BjB;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,iIAqZvB,CAAC"}
1
+ {"version":3,"file":"CrudStore.d.ts","sourceRoot":"","sources":["../src/CrudStore.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAEV,SAAS,EACT,WAAW,EAIZ,MAAM,SAAS,CAAC;AA0BjB;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,iIAiZvB,CAAC"}
package/dist/CrudStore.js CHANGED
@@ -1 +1 @@
1
- import{createDoNotDevStore as m}from"@donotdev/core";const u={filters:{},showFavoritesOnly:!1};function r(){return{loading:!1,error:null,lastUpdated:0,optimistic:{},ui:{...u}}}const d={backend:null,crudService:null,hideSuccessToasts:!1,collections:{}},f=m({name:"crud-store",createStore:(n,s)=>({...d,setCrudService:t=>{n({crudService:t})},setBackend:t=>{n({backend:t})},setHideSuccessToasts:t=>{n({hideSuccessToasts:t})},setLoading:(t,c)=>{n(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,loading:c}}}})},setError:(t,c)=>{n(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,error:c,loading:!1}}}})},setFilters:(t,c)=>{n(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,ui:{...o.ui,filters:c}}}}})},setShowFavoritesOnly:(t,c)=>{n(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,ui:{...o.ui,showFavoritesOnly:c}}}}})},getFilters:t=>s().collections[t]?.ui?.filters||{},getShowFavoritesOnly:t=>s().collections[t]?.ui?.showFavoritesOnly||!1,clearCollection:t=>{n(c=>{const{[t]:i,...o}=c.collections;return{collections:o}})},clearError:t=>{n(c=>{const i=c.collections[t];return i?{collections:{...c.collections,[t]:{...i,error:null}}}:c})},addOptimistic:(t,c,i)=>{n(o=>{const e=o.collections[t]||r();return{collections:{...o.collections,[t]:{...e,optimistic:{...e.optimistic,[c]:{tempId:c,originalData:null,optimisticData:i,status:"pending",operation:"add"}},lastUpdated:Date.now()}}}})},confirmOptimistic:(t,c,i,o)=>{n(e=>{const l=e.collections[t];if(!l)return e;const{[c]:a,...p}=l.optimistic;return{collections:{...e.collections,[t]:{...l,optimistic:p,lastUpdated:Date.now()}}}})},rejectOptimistic:(t,c)=>{n(i=>{const o=i.collections[t];if(!o)return i;const{[c]:e,...l}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:l,lastUpdated:Date.now()}}}})},updateOptimistic:(t,c,i,o)=>{n(e=>{const l=e.collections[t]||r();return{collections:{...e.collections,[t]:{...l,optimistic:{...l.optimistic,[c]:{tempId:c,originalData:o,optimisticData:i,status:"pending",operation:"update"}},lastUpdated:Date.now()}}}})},confirmUpdate:(t,c)=>{n(i=>{const o=i.collections[t];if(!o)return i;const{[c]:e,...l}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:l}}}})},rejectUpdate:(t,c)=>{n(i=>{const o=i.collections[t];if(!o)return i;const e=o.optimistic[c];if(!e||e.operation!=="update")return i;const{[c]:l,...a}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:a,lastUpdated:Date.now()}}}})},deleteOptimistic:(t,c,i)=>{n(o=>{const e=o.collections[t];return e?{collections:{...o.collections,[t]:{...e,optimistic:{...e.optimistic,[c]:{tempId:c,originalData:i,optimisticData:null,status:"pending",operation:"delete"}},lastUpdated:Date.now()}}}:o})},confirmDelete:(t,c)=>{n(i=>{const o=i.collections[t];if(!o)return i;const{[c]:e,...l}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:l}}}})},rejectDelete:(t,c)=>{n(i=>{const o=i.collections[t];if(!o)return i;const e=o.optimistic[c];if(!e||e.operation!=="delete")return i;const{[c]:l,...a}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:a,lastUpdated:Date.now()}}}})},getLoading:t=>s().collections[t]?.loading||!1,getError:t=>s().collections[t]?.error||null,isOptimistic:(t,c)=>s().collections[t]?.optimistic[c]?.status==="pending",getOptimisticStatus:(t,c)=>s().collections[t]?.optimistic[c]?.status??null,getOptimisticData:(t,c)=>s().collections[t]?.optimistic[c]?.optimisticData??null})});export{f as useCrudStore};
1
+ import{createDoNotDevStore as d}from"@donotdev/core";import{OPTIMISTIC_STATUSES as u,CRUD_OPERATION as a}from"./types";const D={filters:{},showFavoritesOnly:!1};function r(){return{loading:!1,error:null,lastUpdated:0,optimistic:{},ui:{...D}}}const f={crudService:null,hideSuccessToasts:!1,collections:{}},E=d({name:"crud-store",createStore:(l,n)=>({...f,setCrudService:t=>{l({crudService:t})},setHideSuccessToasts:t=>{l({hideSuccessToasts:t})},setLoading:(t,c)=>{l(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,loading:c}}}})},setError:(t,c)=>{l(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,error:c,loading:!1}}}})},setFilters:(t,c)=>{l(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,ui:{...o.ui,filters:c}}}}})},setShowFavoritesOnly:(t,c)=>{l(i=>{const o=i.collections[t]||r();return{collections:{...i.collections,[t]:{...o,ui:{...o.ui,showFavoritesOnly:c}}}}})},getFilters:t=>n().collections[t]?.ui?.filters||{},getShowFavoritesOnly:t=>n().collections[t]?.ui?.showFavoritesOnly||!1,clearCollection:t=>{l(c=>{const{[t]:i,...o}=c.collections;return{collections:o}})},clearError:t=>{l(c=>{const i=c.collections[t];return i?{collections:{...c.collections,[t]:{...i,error:null}}}:c})},addOptimistic:(t,c,i)=>{l(o=>{const e=o.collections[t]||r();return{collections:{...o.collections,[t]:{...e,optimistic:{...e.optimistic,[c]:{tempId:c,originalData:null,optimisticData:i,status:u.PENDING,operation:a.ADD}},lastUpdated:Date.now()}}}})},confirmOptimistic:(t,c,i,o)=>{l(e=>{const s=e.collections[t];if(!s)return e;const{[c]:p,...m}=s.optimistic;return{collections:{...e.collections,[t]:{...s,optimistic:m,lastUpdated:Date.now()}}}})},rejectOptimistic:(t,c)=>{l(i=>{const o=i.collections[t];if(!o)return i;const{[c]:e,...s}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:s,lastUpdated:Date.now()}}}})},updateOptimistic:(t,c,i,o)=>{l(e=>{const s=e.collections[t]||r();return{collections:{...e.collections,[t]:{...s,optimistic:{...s.optimistic,[c]:{tempId:c,originalData:o,optimisticData:i,status:u.PENDING,operation:a.UPDATE}},lastUpdated:Date.now()}}}})},confirmUpdate:(t,c)=>{l(i=>{const o=i.collections[t];if(!o)return i;const{[c]:e,...s}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:s}}}})},rejectUpdate:(t,c)=>{l(i=>{const o=i.collections[t];if(!o)return i;const e=o.optimistic[c];if(!e||e.operation!==a.UPDATE)return i;const{[c]:s,...p}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:p,lastUpdated:Date.now()}}}})},deleteOptimistic:(t,c,i)=>{l(o=>{const e=o.collections[t];return e?{collections:{...o.collections,[t]:{...e,optimistic:{...e.optimistic,[c]:{tempId:c,originalData:i,optimisticData:null,status:u.PENDING,operation:a.DELETE}},lastUpdated:Date.now()}}}:o})},confirmDelete:(t,c)=>{l(i=>{const o=i.collections[t];if(!o)return i;const{[c]:e,...s}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:s}}}})},rejectDelete:(t,c)=>{l(i=>{const o=i.collections[t];if(!o)return i;const e=o.optimistic[c];if(!e||e.operation!==a.DELETE)return i;const{[c]:s,...p}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:p,lastUpdated:Date.now()}}}})},getLoading:t=>n().collections[t]?.loading||!1,getError:t=>n().collections[t]?.error||null,isOptimistic:(t,c)=>n().collections[t]?.optimistic[c]?.status==="pending",getOptimisticStatus:(t,c)=>n().collections[t]?.optimistic[c]?.status??null,getOptimisticData:(t,c)=>n().collections[t]?.optimistic[c]?.optimisticData??null})});export{E as useCrudStore};
@@ -7,36 +7,10 @@
7
7
  * @author AMBROISE PARK Consulting
8
8
  */
9
9
  import type { EntityField, FieldType, ValueTypeForField } from '@donotdev/core';
10
- import type { ComponentType } from 'react';
11
- import type { Control, ControllerRenderProps, FieldErrors, FieldValues, Path } from 'react-hook-form';
12
- /**
13
- * Props for controlled field components (react-hook-form)
14
- *
15
- * Use framework's `useController` hook (from @donotdev/crud) instead of react-hook-form's useController
16
- * to ensure type compatibility.
17
- */
18
- export interface ControlledFieldProps<T extends FieldValues = FieldValues, FT extends FieldType = FieldType> {
19
- /** Control object from react-hook-form - use with framework's useController hook */
20
- control: Control<T>;
21
- errors: FieldErrors<T>;
22
- fieldConfig: EntityField<FT>;
23
- t: (key: string, options?: Record<string, unknown>) => string;
24
- onChange?: (value: ValueTypeForField<FT>) => void;
25
- placeholder?: string;
26
- /** Field object from react-hook-form (provided by framework, no need to use useController) */
27
- field?: ControllerRenderProps<T, Path<T>>;
28
- /** Field state from react-hook-form (provided by framework, no need to use useController) */
29
- fieldState?: {
30
- error?: {
31
- message?: string;
32
- };
33
- isDirty: boolean;
34
- isTouched: boolean;
35
- invalid: boolean;
36
- };
37
- /** Form ID for upload tracking - used by file upload fields */
38
- formId?: string;
39
- }
10
+ import type { ComponentType, ReactElement } from 'react';
11
+ import type { FieldValues } from 'react-hook-form';
12
+ import type { ControlledFieldProps } from './components/controlled/types';
13
+ export type { ControlledFieldProps };
40
14
  /**
41
15
  * Props for uncontrolled field components
42
16
  */
@@ -50,14 +24,31 @@ export interface UncontrolledFieldProps<FT extends FieldType = FieldType> {
50
24
  t: (key: string) => string;
51
25
  config: EntityField<FT>;
52
26
  }
27
+ /**
28
+ * Display formatter for list/card/display views (same signature as fieldTypeRegistry.DisplayFormatter)
29
+ */
30
+ export type CustomDisplayFormatter = (value: unknown, config: EntityField, t: (key: string, options?: Record<string, unknown>) => string, options?: {
31
+ compact?: boolean;
32
+ asString?: boolean;
33
+ }) => string | ReactElement;
34
+ /** Filter UI type for EntityFilters; must match fieldTypeRegistry.FilterType */
35
+ export type CustomFilterType = 'text' | 'range' | 'select' | 'none' | 'address' | 'multiselect';
53
36
  /**
54
37
  * Registration for custom field types (UI components only)
55
38
  * Schemas must be defined in entity.validation.schema
39
+ * Optional displayFormatter so list/card/display views can render the value.
40
+ * Optional filterable + filterType so EntityFilters can show the right filter UI.
56
41
  */
57
42
  export interface FieldTypeRegistration<FT extends string = string> {
58
43
  type: FT;
59
- controlledComponent: ComponentType<ControlledFieldProps<any, any>>;
44
+ controlledComponent: ComponentType<ControlledFieldProps<FieldValues, FieldType>>;
60
45
  uncontrolledComponent?: ComponentType<UncontrolledFieldProps>;
46
+ /** Formatter for list/card/display; receives (value, fieldConfig, t, options?) */
47
+ displayFormatter?: CustomDisplayFormatter;
48
+ /** Whether this field is filterable in list/card filters */
49
+ filterable?: boolean;
50
+ /** Filter UI type for EntityFilters (e.g. 'text', 'range', 'select') */
51
+ filterType?: CustomFilterType;
61
52
  }
62
53
  /**
63
54
  * Field Registry - UI components only
@@ -65,11 +56,13 @@ export interface FieldTypeRegistration<FT extends string = string> {
65
56
  */
66
57
  declare class FieldRegistry {
67
58
  private readonly components;
59
+ private readonly displayFormatters;
60
+ private readonly filterMetadata;
68
61
  /**
69
62
  * Register UI components for a field type (built-in types)
70
63
  * Does NOT register schema - use for types where schema is in @donotdev/core
71
64
  */
72
- registerComponent(type: string, controlled: ComponentType<ControlledFieldProps<any, any>>, uncontrolled?: ComponentType<UncontrolledFieldProps>): void;
65
+ registerComponent(type: string, controlled: ComponentType<ControlledFieldProps<FieldValues, FieldType>>, uncontrolled?: ComponentType<UncontrolledFieldProps>): void;
73
66
  /**
74
67
  * Register a custom field type (UI components only)
75
68
  * Schemas must be defined in entity.validation.schema
@@ -83,6 +76,18 @@ declare class FieldRegistry {
83
76
  * Get uncontrolled component
84
77
  */
85
78
  getUncontrolledComponent(type: string): ComponentType<UncontrolledFieldProps> | undefined;
79
+ /**
80
+ * Get display formatter for a custom field type (registered via registerFieldType with displayFormatter)
81
+ */
82
+ getDisplayFormatter(type: string): CustomDisplayFormatter | undefined;
83
+ /**
84
+ * Get filter type for a custom field type (registered via registerFieldType with filterType)
85
+ */
86
+ getFilterType(type: string): CustomFilterType | undefined;
87
+ /**
88
+ * Check if a custom field type is filterable
89
+ */
90
+ isFilterable(type: string): boolean;
86
91
  /**
87
92
  * Check if type is registered
88
93
  */
@@ -104,7 +109,7 @@ declare class FieldRegistry {
104
109
  */
105
110
  export declare function getFieldRegistry(): FieldRegistry;
106
111
  /**
107
- * Register a custom field type (UI component only)
112
+ * Register a custom field type (form, optional display, optional filter).
108
113
  * Schema must be defined in entity.validation.schema
109
114
  *
110
115
  * @example
@@ -113,41 +118,37 @@ export declare function getFieldRegistry(): FieldRegistry;
113
118
  * import type { ControlledFieldProps } from '@donotdev/crud';
114
119
  * import * as v from 'valibot';
115
120
  *
116
- * // Custom controlled component MUST use framework's useController (not react-hook-form)
117
121
  * function ControlledRepairOperationsField({
118
122
  * fieldConfig,
119
123
  * control,
120
124
  * errors,
121
125
  * t,
122
126
  * }: ControlledFieldProps) {
123
- * // REQUIRED: Use framework's useController - ensures type compatibility
124
127
  * const { field, fieldState } = useController({
125
128
  * name: fieldConfig.name,
126
129
  * control: control,
127
130
  * });
128
- *
129
- * // Use field.value and field.onChange
130
131
  * return (
131
132
  * <div>
132
133
  * Your custom UI here
134
+ * {fieldState?.error && <span>{fieldState.error.message}</span>}
133
135
  * </div>
134
136
  * );
135
137
  * }
136
138
  *
137
- * // Register UI component
138
139
  * registerFieldType({
139
140
  * type: 'repairOperations',
140
141
  * controlledComponent: ControlledRepairOperationsField,
142
+ * displayFormatter: (value) => (value != null ? String(value) : '—'),
143
+ * filterable: true,
144
+ * filterType: 'text',
141
145
  * });
142
146
  *
143
- * // Then define schema in entity
144
147
  * const carEntity = defineEntity({
145
148
  * fields: {
146
149
  * repairs: {
147
150
  * type: 'repairOperations',
148
- * validation: {
149
- * schema: v.array(v.object({ operation: v.string(), cost: v.number() }))
150
- * }
151
+ * validation: { schema: v.array(v.object({ operation: v.string(), cost: v.number() })) }
151
152
  * }
152
153
  * }
153
154
  * });
@@ -158,5 +159,4 @@ export declare function registerFieldType<FT extends string>(registration: Field
158
159
  * Check if a field type is registered
159
160
  */
160
161
  export declare function isFieldTypeRegistered(type: string): boolean;
161
- export {};
162
162
  //# sourceMappingURL=FieldRegistry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FieldRegistry.d.ts","sourceRoot":"","sources":["../src/FieldRegistry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FieldRegistry.d.ts","sourceRoot":"","sources":["../src/FieldRegistry.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- "use client";class i{components=new Map;registerComponent(e,n,o){this.components.set(e,{controlled:n,uncontrolled:o})}register(e){const{type:n,controlledComponent:o,uncontrolledComponent:l}=e;this.components.set(n,{controlled:o,uncontrolled:l})}getControlledComponent(e){return this.components.get(e)?.controlled}getUncontrolledComponent(e){return this.components.get(e)?.uncontrolled}has(e){return this.components.has(e)}getRegisteredTypes(){return Array.from(this.components.keys())}clear(){this.components.clear()}}let r=null;function s(){return r||(r=new i),r}function c(t){s().register(t)}function p(t){return s().has(t)}export{s as getFieldRegistry,p as isFieldTypeRegistered,c as registerFieldType};
1
+ "use client";class c{components=new Map;displayFormatters=new Map;filterMetadata=new Map;registerComponent(e,t,n){this.components.set(e,{controlled:t,uncontrolled:n})}register(e){const{type:t,controlledComponent:n,uncontrolledComponent:p,displayFormatter:l,filterable:i,filterType:a}=e;this.components.set(t,{controlled:n,uncontrolled:p}),l&&this.displayFormatters.set(t,l),(i!==void 0||a!==void 0)&&this.filterMetadata.set(t,{filterable:i??!1,filterType:a})}getControlledComponent(e){return this.components.get(e)?.controlled}getUncontrolledComponent(e){return this.components.get(e)?.uncontrolled}getDisplayFormatter(e){return this.displayFormatters.get(e)}getFilterType(e){return this.filterMetadata.get(e)?.filterType}isFilterable(e){return this.filterMetadata.get(e)?.filterable??!1}has(e){return this.components.has(e)}getRegisteredTypes(){return Array.from(this.components.keys())}clear(){this.components.clear(),this.displayFormatters.clear(),this.filterMetadata.clear()}}let o=null;function s(){return o||(o=new c),o}function d(r){s().register(r)}function m(r){return s().has(r)}export{s as getFieldRegistry,m as isFieldTypeRegistered,d as registerFieldType};
@@ -1,4 +1,4 @@
1
- import type { dndevSchema } from '@donotdev/core';
1
+ import type { dndevSchema, ICrudAdapter, ListSchemaType, PaginatedQueryResult } from '@donotdev/core';
2
2
  /**
3
3
  * Query options for Functions queries
4
4
  *
@@ -9,43 +9,37 @@ import type { dndevSchema } from '@donotdev/core';
9
9
  export interface FunctionsQueryOptions {
10
10
  where?: Array<{
11
11
  field: string;
12
- operator: any;
13
- value: any;
12
+ operator: string;
13
+ value: unknown;
14
14
  }>;
15
15
  orderBy?: Array<{
16
16
  field: string;
17
17
  direction?: 'asc' | 'desc';
18
18
  }>;
19
19
  limit?: number;
20
+ /** Cursor from previous page (QueryOptions uses startAfter; server uses startAfterId) */
21
+ startAfter?: string | null;
20
22
  startAfterId?: string;
21
23
  }
22
- export interface PaginatedQueryResult<T> {
23
- items: T[];
24
- total?: number;
25
- hasMore?: boolean;
26
- lastVisible?: string | null;
27
- }
28
- /**
29
- * Functions backend adapter
30
- * Works with any collection dynamically (collection name passed per operation)
31
- * Assumes Functions are named: `get_${collection}`, `create_${collection}`, etc.
32
- *
33
- * @version 0.0.1
34
- * @since 0.0.1
35
- * @author AMBROISE PARK Consulting
36
- */
37
- export declare class FunctionsAdapter {
38
- private functions;
39
- private region;
40
- constructor();
41
- private ensureFunctions;
24
+ export declare class FunctionsAdapter implements ICrudAdapter {
25
+ readonly serverSideOnly = true;
26
+ /**
27
+ * Resolve ICallableProvider: registry or lazy Firebase fallback (dynamic import for Supabase-only apps).
28
+ */
29
+ private getCallable;
30
+ /**
31
+ * Get callable function invoker. Always uses ICallableProvider (configured or Firebase fallback).
32
+ */
33
+ private getCallableFn;
42
34
  get<T>(collectionName: string, id: string, schema: dndevSchema<unknown>): Promise<T | null>;
43
35
  set<T>(collectionName: string, id: string, data: T, schema: dndevSchema<T>): Promise<void>;
44
36
  update<T>(collectionName: string, id: string, data: Partial<T>): Promise<void>;
45
37
  delete(collectionName: string, id: string): Promise<void>;
46
- add<T>(collectionName: string, data: T, schema: dndevSchema<T>): Promise<string>;
47
- query<T>(collectionName: string, options: FunctionsQueryOptions, schema: dndevSchema<unknown>, schemaType?: 'list' | 'listCard'): Promise<PaginatedQueryResult<T>>;
48
- listCard<T>(collectionName: string, options: FunctionsQueryOptions, schema: dndevSchema<T>): Promise<T[]>;
38
+ add<T>(collectionName: string, data: T, schema: dndevSchema<T>): Promise<{
39
+ id: string;
40
+ data: Record<string, unknown>;
41
+ }>;
42
+ query<T>(collectionName: string, options: FunctionsQueryOptions, schema: dndevSchema<unknown>, schemaType?: ListSchemaType): Promise<PaginatedQueryResult<T>>;
49
43
  subscribe<T>(): () => void;
50
44
  subscribeToCollection<T>(): () => void;
51
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FunctionsAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/FunctionsAdapter.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IAC5D,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,MAAM,CAAS;;YAOT,eAAe;IAUvB,GAAG,CAAC,CAAC,EACT,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAC3B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAed,GAAG,CAAC,CAAC,EACT,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,CAAC,EACP,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,IAAI,CAAC;IAeV,MAAM,CAAC,CAAC,EACZ,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GACf,OAAO,CAAC,IAAI,CAAC;IAcV,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAczD,GAAG,CAAC,CAAC,EACT,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,CAAC,EACP,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,MAAM,CAAC;IAgBZ,KAAK,CAAC,CAAC,EACX,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAC5B,UAAU,GAAE,MAAM,GAAG,UAAmB,GACvC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC;IA2D7B,QAAQ,CAAC,CAAC,EACd,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,CAAC,EAAE,CAAC;IAgDf,SAAS,CAAC,CAAC,KAAK,MAAM,IAAI;IAM1B,qBAAqB,CAAC,CAAC,KAAK,MAAM,IAAI;CAKvC"}
1
+ {"version":3,"file":"FunctionsAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/FunctionsAdapter.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,WAAW,EAEX,YAAY,EACZ,cAAc,EACd,oBAAoB,EACrB,MAAM,gBAAgB,CAAC;AAGxB;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACnE,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAgBD,qBAAa,gBAAiB,YAAW,YAAY;IACnD,QAAQ,CAAC,cAAc,QAAQ;IAE/B;;OAEG;YACW,WAAW;IAWzB;;OAEG;YACW,aAAa;IASrB,GAAG,CAAC,CAAC,EACT,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAC3B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAgBd,GAAG,CAAC,CAAC,EACT,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,CAAC,EACP,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,IAAI,CAAC;IAWV,MAAM,CAAC,CAAC,EACZ,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GACf,OAAO,CAAC,IAAI,CAAC;IAUV,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUzD,GAAG,CAAC,CAAC,EACT,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,CAAC,EACP,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IAmBnD,KAAK,CAAC,CAAC,EACX,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAC5B,UAAU,GAAE,cAAsC,GACjD,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC;IAgEnC,SAAS,CAAC,CAAC,KAAK,MAAM,IAAI;IAS1B,qBAAqB,CAAC,CAAC,KAAK,MAAM,IAAI;CAQvC"}