@donotdev/crud 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -19
- package/dist/CrudService.d.ts +84 -11
- package/dist/CrudService.d.ts.map +1 -1
- package/dist/CrudService.js +9 -1
- package/dist/CrudStore.d.ts.map +1 -1
- package/dist/CrudStore.js +1 -1
- package/dist/FieldRegistry.d.ts +40 -13
- package/dist/FieldRegistry.d.ts.map +1 -1
- package/dist/FieldRegistry.js +1 -1
- package/dist/adapters/FunctionsAdapter.d.ts +20 -26
- package/dist/adapters/FunctionsAdapter.d.ts.map +1 -1
- package/dist/adapters/FunctionsAdapter.js +1 -1
- package/dist/adapters/index.d.ts +0 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/components/CrudCard.d.ts +8 -0
- package/dist/components/CrudCard.d.ts.map +1 -0
- package/dist/components/CrudCard.js +1 -0
- package/dist/components/DateFilter.d.ts.map +1 -1
- package/dist/components/DisplayThumbnail.d.ts +29 -0
- package/dist/components/DisplayThumbnail.d.ts.map +1 -0
- package/dist/components/DisplayThumbnail.js +1 -0
- package/dist/components/EntityFilters.d.ts.map +1 -1
- package/dist/components/EntityFilters.js +1 -1
- package/dist/components/FormLayout.d.ts.map +1 -1
- package/dist/components/__tests__/EntityFilters.test.d.ts +2 -0
- package/dist/components/__tests__/EntityFilters.test.d.ts.map +1 -0
- package/dist/components/__tests__/EntityFilters.test.js +1 -0
- package/dist/components/__tests__/FormFieldRenderer.test.d.ts +2 -0
- package/dist/components/__tests__/FormFieldRenderer.test.d.ts.map +1 -0
- package/dist/components/__tests__/FormFieldRenderer.test.js +1 -0
- package/dist/components/controlled/file/ControlledMultiImageField.js +1 -1
- package/dist/components/fields/display/RichTextDisplay.d.ts.map +1 -1
- package/dist/components/fields/display/RichTextDisplay.js +2 -2
- package/dist/components/form/fields/AddressFieldComponent.d.ts.map +1 -1
- package/dist/components/form/fields/AddressFieldComponent.js +1 -1
- package/dist/components/form/fields/SwitchFieldComponent.d.ts.map +1 -1
- package/dist/components/form/fields/internal/TiptapEditor.d.ts +4 -2
- package/dist/components/form/fields/internal/TiptapEditor.d.ts.map +1 -1
- package/dist/components/form/fields/internal/TiptapEditor.js +2 -2
- package/dist/components/form/internal/ImageViewerDialog.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/fieldTypeRegistry.d.ts +3 -0
- package/dist/fieldTypeRegistry.d.ts.map +1 -1
- package/dist/fieldTypeRegistry.js +1 -1
- package/dist/forms/hooks/useEntityForm.d.ts.map +1 -1
- package/dist/forms/hooks/useEntityForm.js +1 -1
- package/dist/hooks/useCrudFilters.d.ts +0 -15
- package/dist/hooks/useCrudFilters.d.ts.map +1 -1
- package/dist/hooks/useCrudFilters.js +1 -1
- package/dist/hooks/useFileUpload.d.ts.map +1 -1
- package/dist/hooks/useFileUpload.js +1 -1
- package/dist/hooks/useFormNavigationGuard.d.ts.map +1 -1
- package/dist/hooks/useFormNavigationGuard.js +1 -1
- package/dist/hooks/useRelatedItems.d.ts +1 -2
- package/dist/hooks/useRelatedItems.d.ts.map +1 -1
- package/dist/hooks/useRelatedItems.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/registerBuiltinFieldTypes.d.ts.map +1 -1
- package/dist/registerBuiltinFieldTypes.js +1 -1
- package/dist/stores/FormStore.d.ts.map +1 -1
- package/dist/stores/FormStore.js +1 -1
- package/dist/stores/UploadStore.d.ts.map +1 -1
- package/dist/stores/UploadStore.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +22 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/useBaseCrudList.d.ts +1 -4
- package/dist/useBaseCrudList.d.ts.map +1 -1
- package/dist/useBaseCrudList.js +1 -1
- package/dist/useCrud.d.ts +1 -2
- package/dist/useCrud.d.ts.map +1 -1
- package/dist/useCrud.js +1 -1
- package/dist/useCrudCardList.d.ts +1 -2
- package/dist/useCrudCardList.d.ts.map +1 -1
- package/dist/useCrudCardList.js +1 -1
- package/dist/useCrudList.d.ts +1 -1
- package/dist/useCrudList.d.ts.map +1 -1
- package/dist/useCrudList.js +1 -1
- package/dist/utils/fileStorage.d.ts +10 -5
- package/dist/utils/fileStorage.d.ts.map +1 -1
- package/dist/utils/fileStorage.js +1 -1
- package/dist/utils/imageStorage.d.ts +9 -4
- package/dist/utils/imageStorage.d.ts.map +1 -1
- package/dist/utils/imageStorage.js +1 -1
- package/dist/utils/mergeWithOptimistic.d.ts.map +1 -1
- package/dist/utils/mergeWithOptimistic.js +1 -1
- package/dist/utils/sanitizeHtml.d.ts +6 -0
- package/dist/utils/sanitizeHtml.d.ts.map +1 -0
- package/dist/utils/sanitizeHtml.js +1 -0
- package/dist/utils/scopeUtils.d.ts +1 -2
- package/dist/utils/scopeUtils.d.ts.map +1 -1
- package/dist/utils/scopeUtils.js +1 -1
- package/dist/utils/uploadValidation.d.ts +5 -4
- package/dist/utils/uploadValidation.d.ts.map +1 -1
- package/dist/utils/uploadValidation.js +1 -1
- package/package.json +12 -6
- package/dist/adapters/FirestoreAdapter.d.ts +0 -65
- package/dist/adapters/FirestoreAdapter.d.ts.map +0 -1
- package/dist/adapters/FirestoreAdapter.js +0 -1
- package/dist/components/EntityDisplayRenderer.d.ts +0 -43
- package/dist/components/EntityDisplayRenderer.d.ts.map +0 -1
- package/dist/components/EntityDisplayRenderer.js +0 -1
- package/dist/components/EntityFormRenderer.d.ts +0 -86
- package/dist/components/EntityFormRenderer.d.ts.map +0 -1
- 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
|
-
|
|
131
|
+
Direct Firestore SDK operations with real-time subscriptions.
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
Direct PostgREST access with RLS-based security.
|
|
149
155
|
|
|
150
|
-
|
|
156
|
+
```typescript
|
|
157
|
+
import { SupabaseCrudAdapter } from '@donotdev/supabase';
|
|
158
|
+
configureProviders({ crud: new SupabaseCrudAdapter(supabaseClient) });
|
|
159
|
+
```
|
|
151
160
|
|
|
152
|
-
|
|
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.
|
package/dist/CrudService.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import type { dndevSchema, QueryClient } from '@donotdev/core';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
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
|
-
|
|
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?:
|
|
37
|
-
queryKey: readonly ["crud", string, "query",
|
|
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?:
|
|
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":"
|
|
1
|
+
{"version":3,"file":"CrudService.d.ts","sourceRoot":"","sources":["../src/CrudService.ts"],"names":[],"mappings":"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"}
|
package/dist/CrudService.js
CHANGED
|
@@ -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 m}from"@donotdev/components";import{createSingleton as A,handleError as f,getQueryClient as L,getI18nInstance as c,hasProvider as D,getProvider as T,hasRestrictedVisibility as q,validateWithSchema as F}from"@donotdev/core";import{LIST_SCHEMA_TYPE as _}from"@donotdev/core";import{CRUD_OPERATION as y}from"./types";const S=1/0,$=1e3*60*30;class b{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,s,i){this.security&&this.security.audit({type:t,collection:e,docId:s,userId:i??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&&q(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(!D("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=T("crud"),this.store&&this.store.getState().setCrudService(this)}}getQueryClient(){return L()}getListQueryOptions(t,e,s,i,a=_.LIST){const r=this.adapter,u=()=>this._ensureAdapter();return{queryKey:["crud",t,"query",JSON.stringify(e)],queryFn:async()=>{if(r||await u(),!this.adapter)throw new Error("Adapter not initialized");return this.adapter.query(t,e,s,a)},staleTime:i?.staleTime??S}}getDocQueryOptions(t,e,s,i){const a=this.adapter,r=()=>this._ensureAdapter();return{queryKey:["crud",t,"get",e],queryFn:async()=>{if(a||await r(),!this.adapter)throw new Error("Adapter not initialized");return this.adapter.get(t,e,s)},staleTime:i?.staleTime??S}}_updateGetCache(t,e,s,i){const a=this.getQueryClient(),r=["crud",t,"get",e];i===y.DELETE?a.removeQueries({queryKey:r}):i===y.SET&&s?a.setQueryData(r,s):(i===y.UPDATE||i===y.ADD)&&s&&a.setQueryData(r,u=>u?{...u,...s}:s)}_updateListCaches(t,e,s,i){const a=this.getQueryClient();this._updateGetCache(t,e,s,i),a.setQueriesData({queryKey:["crud",t]},r=>{if(r&&typeof r=="object"&&"items"in r){const u=r;let h=u.items;return i===y.DELETE?(h=u.items.filter(d=>d!=null&&typeof d=="object"&&d.id!==e),{...u,items:h,total:Math.max(0,(u.total??h.length)-1)}):i===y.ADD&&s?(h=[...u.items,{...s,id:e}],{...u,items:h,total:(u.total??u.items.length)+1}):(i===y.UPDATE||i===y.SET)&&s&&u.items.some(n=>n!=null&&typeof n=="object"&&n.id===e)?(h=u.items.map(n=>n==null||typeof n!="object"||n.id!==e?n:i===y.SET?{...s,id:e}:{...n,...s}),{...u,items:h}):r}if(Array.isArray(r)){if(i===y.DELETE)return r.filter(u=>u.id!==e);if(i===y.ADD&&s)return[...r,{...s,id:e}];if((i===y.UPDATE||i===y.SET)&&s)return r.some(h=>h!=null&&typeof h=="object"&&h.id===e)?r.map(h=>h==null||typeof h!="object"||h.id!==e?h:i===y.SET?{...s,id:e}:{...h,...s}):r}return r})}_checkUniquenessFromCache(t,e,s){const a=s.metadata?.uniqueKeys;if(!a||a.length===0)return null;const u=this.getQueryClient().getQueriesData({queryKey:["crud",t]}),h=[];for(const[,n]of u)n&&typeof n=="object"&&"items"in n?h.push(...n.items):Array.isArray(n)&&h.push(...n);if(h.length===0)return null;const d=e;for(const n of a){if(!n.fields.every(p=>d[p]!=null&&d[p]!==""))continue;const g=h.find(p=>n.fields.every(w=>{const C=typeof d[w]=="string"?d[w].toLowerCase():d[w],E=typeof p[w]=="string"?p[w].toLowerCase():p[w];return C===E}));if(g){if(n.findOrCreate)return g.id;const p=n.fields.join(" + ");throw f(new Error(n.errorMessage||`Duplicate ${p}`),{userMessage:n.errorMessage||`A record with this ${p} already exists`,showNotification:!0})}}return null}_getItemFromListCache(t,e){const i=this.getQueryClient().getQueriesData({queryKey:["crud",t]});for(const[,a]of i)if(a?.items){const r=a.items.find(u=>u.id===e);if(r)return r}return null}async invalidateCollection(t){await this.getQueryClient().invalidateQueries({queryKey:["crud",t]})}async get(t,e,s,i){if(await this._ensureAdapter(),i?.noCache)return this._getFromAdapter(t,e,s);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,s,!1),staleTime:i?.staleTime??S})}catch(a){const r=f(a,{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,s,i=!0){this.store&&i&&(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,s),await this.adapter.get(t,e,s)}catch(a){const r=f(a,{userMessage:`Failed to fetch ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,r),r}finally{this.store&&i&&this.store.getState().setLoading(t,!1)}}async query(t,e,s,i,a=_.LIST){if(await this._ensureAdapter(),i?.noCache)return this._queryFromAdapter(t,e,s,!0,a);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,s,!1,a),staleTime:i?.staleTime??S})}catch(r){const u=f(r,{userMessage:`Failed to query ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,u),u}finally{this.store&&this.store.getState().setLoading(t,!1)}}async _queryFromAdapter(t,e,s,i=!0,a=_.LIST){this.store&&i&&(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,s),await this.adapter.query(t,e,s,a)}catch(r){const u=f(r,{userMessage:`Failed to query ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,u),u}finally{this.store&&i&&this.store.getState().setLoading(t,!1)}}_getEntityName(t){const e=c();let s=`entity-${t}`,i=e.t("name",{ns:s,defaultValue:null});return i&&i!=="name"&&i!==`${s}:name`||t.endsWith("s")&&t.length>1&&(s=`entity-${t.slice(0,-1)}`,i=e.t("name",{ns:s,defaultValue:null}),i&&i!=="name"&&i!==`${s}:name`)?i:t}async set(t,e,s,i,a){await this._ensureAdapter(),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const u=this.getQueryClient().getQueryData(["crud",t,"get",e])??null;this.store&&this.store.getState().updateOptimistic(t,e,s,u);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 h=this._getPiiFields(i),d=h.length>0&&this.security?this.security.encryptPii(s,h):s;if(await this.adapter.set(t,e,d,i),this._updateListCaches(t,e,s,y.SET),this.store&&this.store.getState().confirmUpdate(t,e),this._auditCrud("crud.update",t,e),this._shouldShowSuccessToast(a)){const n=c(),o=this._getEntityName(t);m("success",n.t("messages.updateSuccess",{ns:"crud",entity:o}))}}catch(h){u&&this._updateListCaches(t,e,u,y.SET),this.store&&this.store.getState().rejectUpdate(t,e);const d=f(h,{userMessage:`Failed to save ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,d),d}finally{this.store&&this.store.getState().setLoading(t,!1)}}async update(t,e,s,i,a){await this._ensureAdapter(),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const u=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e),h=u?{...u,...s}:{...s,id:e};this.store&&this.store.getState().updateOptimistic(t,e,h,u??null);try{if(!this.adapter)throw new Error("Adapter not initialized");i&&(this._assertCanAccess(t,i),F(i,s,`CrudService.update(${t})`)),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const d=i?this._getPiiFields(i):[],n=d.length>0&&this.security?this.security.encryptPii(s,d):s;if(await this.adapter.update(t,e,n),this._updateListCaches(t,e,h,y.UPDATE),this.store&&this.store.getState().confirmUpdate(t,e),this._auditCrud("crud.update",t,e),this._shouldShowSuccessToast(a)){const o=c(),g=this._getEntityName(t);m("success",o.t("messages.updateSuccess",{ns:"crud",entity:g}))}}catch(d){u?this._updateListCaches(t,e,u,y.UPDATE):this.invalidateCollection(t),this.store&&this.store.getState().rejectUpdate(t,e);const n=f(d,{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,s){await this._ensureAdapter(),this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const a=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e);this.store&&a&&this.store.getState().deleteOptimistic(t,e,a);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,y.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(s)){const r=c(),u=this._getEntityName(t);m("success",r.t("messages.deleteSuccess",{ns:"crud",entity:u}))}}catch(r){a&&this._updateListCaches(t,e,a,y.ADD),this.store&&this.store.getState().rejectDelete(t,e);const u=f(r,{userMessage:`Failed to delete ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,u),u}finally{this.store&&this.store.getState().setLoading(t,!1)}}async add(t,e,s,i){await this._ensureAdapter();const a=this._checkUniquenessFromCache(t,e,s);if(a)return a;this.store&&(this.store.getState().setLoading(t,!0),this.store.getState().setError(t,null));const r=`temp_${crypto.randomUUID()}`;this.store&&this.store.getState().addOptimistic(t,r,{...e,id:r});try{if(!this.adapter)throw new Error("Adapter not initialized");this._assertCanAccess(t,s),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const u=this._getPiiFields(s),h=u.length>0&&this.security?this.security.encryptPii(e,u):e,d=await this.adapter.add(t,h,s),n=d.id,o=d.data;if(this._updateListCaches(t,r,null,y.DELETE),this._updateListCaches(t,n,o,y.ADD),this.store&&this.store.getState().confirmOptimistic(t,r,n,o),this._auditCrud("crud.create",t,n),this._shouldShowSuccessToast(i)){const g=c(),p=this._getEntityName(t);m("success",g.t("messages.createSuccess",{ns:"crud",entity:p}))}return n}catch(u){this._updateListCaches(t,r,null,y.DELETE),this.store&&this.store.getState().rejectOptimistic(t,r);const h=f(u,{userMessage:`Failed to create ${t}`,showNotification:!0});throw this.store&&this.store.getState().setError(t,h),h}finally{this.store&&this.store.getState().setLoading(t,!1)}}subscribe(t,e,s,i){if(!this.adapter){let a=!1,r=null;return this._ensureAdapter().then(()=>{a||(r=this._subscribeSync(t,e,s,i))}).catch(u=>{a||s(null,f(u))}),()=>{a=!0,r?.()}}return this._subscribeSync(t,e,s,i)}_subscribeSync(t,e,s,i){try{const a=this.adapter;return a?a.subscribe?(this._assertCanAccess(t,i),a.subscribe(t,e,(r,u)=>{r&&this.getQueryClient().setQueryData(["crud",t,"get",e],r),s(r,u)},i)):()=>{}:(s(null,new Error("Adapter not initialized")),()=>{})}catch(a){return s(null,f(a)),()=>{}}}subscribeToCollection(t,e,s,i){if(!this.adapter){let a=!1,r=null;return this._ensureAdapter().then(()=>{a||(r=this._subscribeToCollectionSync(t,e,s,i))}).catch(u=>{a||s([],f(u))}),()=>{a=!0,r?.()}}return this._subscribeToCollectionSync(t,e,s,i)}_subscribeToCollectionSync(t,e,s,i){try{const a=this.adapter;return a?a.subscribeToCollection?(this._assertCanAccess(t,i),a.subscribeToCollection(t,e,(r,u)=>{r&&this.getQueryClient().setQueryData(["crud",t,"query",JSON.stringify(e)],h=>h?{...h,items:r}:{items:r}),s(r,u)},i)):()=>{}:(s([],new Error("Adapter not initialized")),()=>{})}catch(a){return s([],f(a)),()=>{}}}async addOptimistic(t,e,s,i){await this._ensureAdapter();const a=this._checkUniquenessFromCache(t,e,s);if(a)return{...e,id:a};this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const r=`temp_${crypto.randomUUID()}`,u={...e,id:r,_optimistic:!0};this.store&&this.store.getState().addOptimistic(t,r,u);try{if(!this.adapter)throw new Error("Adapter not initialized");const h=await this.adapter.add(t,e,s),d=h.id,n=h.data;if(this._updateListCaches(t,r,null,y.DELETE),this._updateListCaches(t,d,n,y.ADD),this.store&&this.store.getState().confirmOptimistic(t,r,d,n),this._auditCrud("crud.create",t,d),this._shouldShowSuccessToast(i)){const o=c(),g=this._getEntityName(t);m("success",o.t("messages.createSuccess",{ns:"crud",entity:g}))}return n}catch(h){throw this._updateListCaches(t,r,null,y.DELETE),this.store&&this.store.getState().rejectOptimistic(t,r),f(h,{userMessage:`Failed to create ${t}`,showNotification:!0})}}async updateOptimistic(t,e,s,i,a){await this._ensureAdapter(),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const u=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e),h=u?{...u,...s,_optimistic:!0}:{...s,id:e,_optimistic:!0};this.store&&u&&this.store.getState().updateOptimistic(t,e,h,u);try{if(!this.adapter)throw new Error("Adapter not initialized");await this.adapter.update(t,e,s);const{_optimistic:d,...n}=h;if(this._updateListCaches(t,e,n,y.UPDATE),this.store&&this.store.getState().confirmUpdate(t,e),this._auditCrud("crud.update",t,e),this._shouldShowSuccessToast(a)){const o=c(),g=this._getEntityName(t);m("success",o.t("messages.updateSuccess",{ns:"crud",entity:g}))}return n}catch(d){throw u&&this._updateListCaches(t,e,u,y.UPDATE),this.store&&this.store.getState().rejectUpdate(t,e),f(d,{userMessage:`Failed to update ${t}`,showNotification:!0})}}async deleteOptimistic(t,e,s){await this._ensureAdapter(),this.security&&await this.security.checkRateLimit(this._rateLimitKey(t),"write");const a=this.getQueryClient().getQueryData(["crud",t,"get",e])??this._getItemFromListCache(t,e);this.store&&a&&this.store.getState().deleteOptimistic(t,e,a);try{if(!this.adapter)throw new Error("Adapter not initialized");if(await this.adapter.delete(t,e),this._updateListCaches(t,e,null,y.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(s)){const r=c(),u=this._getEntityName(t);m("success",r.t("messages.deleteSuccess",{ns:"crud",entity:u}))}}catch(r){throw a&&this._updateListCaches(t,e,a,y.ADD),this.store&&this.store.getState().rejectDelete(t,e),f(r,{userMessage:`Failed to delete ${t}`,showNotification:!0})}}}const O=A(()=>new b);export{O as getCrudService};
|
package/dist/CrudStore.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrudStore.d.ts","sourceRoot":"","sources":["../src/CrudStore.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,
|
|
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
|
|
1
|
+
import{createDoNotDevStore as d}from"@donotdev/core";import{OPTIMISTIC_STATUSES as m,CRUD_OPERATION as a}from"./types";const S={filters:{},showFavoritesOnly:!1};function r(){return{loading:!1,error:null,lastUpdated:0,optimistic:{},ui:{...S}}}const D={crudService:null,hideSuccessToasts:!1,collections:{}},E=d({name:"crud-store",createStore:(n,s)=>({...D,setCrudService:t=>{n({crudService: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:m.PENDING,operation:a.ADD}},lastUpdated:Date.now()}}}})},confirmOptimistic:(t,c,i,o)=>{n(e=>{const l=e.collections[t];if(!l)return e;const{[c]:p,...u}=l.optimistic;return{collections:{...e.collections,[t]:{...l,optimistic:u,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:m.PENDING,operation:a.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!==a.UPDATE)return i;const{[c]:l,...p}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:p,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:m.PENDING,operation:a.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!==a.DELETE)return i;const{[c]:l,...p}=o.optimistic;return{collections:{...i.collections,[t]:{...o,optimistic:p,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{E as useCrudStore};
|
package/dist/FieldRegistry.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @author AMBROISE PARK Consulting
|
|
8
8
|
*/
|
|
9
9
|
import type { EntityField, FieldType, ValueTypeForField } from '@donotdev/core';
|
|
10
|
-
import type { ComponentType } from 'react';
|
|
10
|
+
import type { ComponentType, ReactElement } from 'react';
|
|
11
11
|
import type { Control, ControllerRenderProps, FieldErrors, FieldValues, Path } from 'react-hook-form';
|
|
12
12
|
/**
|
|
13
13
|
* Props for controlled field components (react-hook-form)
|
|
@@ -50,14 +50,31 @@ export interface UncontrolledFieldProps<FT extends FieldType = FieldType> {
|
|
|
50
50
|
t: (key: string) => string;
|
|
51
51
|
config: EntityField<FT>;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Display formatter for list/card/display views (same signature as fieldTypeRegistry.DisplayFormatter)
|
|
55
|
+
*/
|
|
56
|
+
export type CustomDisplayFormatter = (value: unknown, config: EntityField, t: (key: string, options?: Record<string, unknown>) => string, options?: {
|
|
57
|
+
compact?: boolean;
|
|
58
|
+
asString?: boolean;
|
|
59
|
+
}) => string | ReactElement;
|
|
60
|
+
/** Filter UI type for EntityFilters; must match fieldTypeRegistry.FilterType */
|
|
61
|
+
export type CustomFilterType = 'text' | 'range' | 'select' | 'none' | 'address' | 'multiselect';
|
|
53
62
|
/**
|
|
54
63
|
* Registration for custom field types (UI components only)
|
|
55
64
|
* Schemas must be defined in entity.validation.schema
|
|
65
|
+
* Optional displayFormatter so list/card/display views can render the value.
|
|
66
|
+
* Optional filterable + filterType so EntityFilters can show the right filter UI.
|
|
56
67
|
*/
|
|
57
68
|
export interface FieldTypeRegistration<FT extends string = string> {
|
|
58
69
|
type: FT;
|
|
59
|
-
controlledComponent: ComponentType<ControlledFieldProps<
|
|
70
|
+
controlledComponent: ComponentType<ControlledFieldProps<FieldValues, FieldType>>;
|
|
60
71
|
uncontrolledComponent?: ComponentType<UncontrolledFieldProps>;
|
|
72
|
+
/** Formatter for list/card/display; receives (value, fieldConfig, t, options?) */
|
|
73
|
+
displayFormatter?: CustomDisplayFormatter;
|
|
74
|
+
/** Whether this field is filterable in list/card filters */
|
|
75
|
+
filterable?: boolean;
|
|
76
|
+
/** Filter UI type for EntityFilters (e.g. 'text', 'range', 'select') */
|
|
77
|
+
filterType?: CustomFilterType;
|
|
61
78
|
}
|
|
62
79
|
/**
|
|
63
80
|
* Field Registry - UI components only
|
|
@@ -65,11 +82,13 @@ export interface FieldTypeRegistration<FT extends string = string> {
|
|
|
65
82
|
*/
|
|
66
83
|
declare class FieldRegistry {
|
|
67
84
|
private readonly components;
|
|
85
|
+
private readonly displayFormatters;
|
|
86
|
+
private readonly filterMetadata;
|
|
68
87
|
/**
|
|
69
88
|
* Register UI components for a field type (built-in types)
|
|
70
89
|
* Does NOT register schema - use for types where schema is in @donotdev/core
|
|
71
90
|
*/
|
|
72
|
-
registerComponent(type: string, controlled: ComponentType<ControlledFieldProps<
|
|
91
|
+
registerComponent(type: string, controlled: ComponentType<ControlledFieldProps<FieldValues, FieldType>>, uncontrolled?: ComponentType<UncontrolledFieldProps>): void;
|
|
73
92
|
/**
|
|
74
93
|
* Register a custom field type (UI components only)
|
|
75
94
|
* Schemas must be defined in entity.validation.schema
|
|
@@ -83,6 +102,18 @@ declare class FieldRegistry {
|
|
|
83
102
|
* Get uncontrolled component
|
|
84
103
|
*/
|
|
85
104
|
getUncontrolledComponent(type: string): ComponentType<UncontrolledFieldProps> | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Get display formatter for a custom field type (registered via registerFieldType with displayFormatter)
|
|
107
|
+
*/
|
|
108
|
+
getDisplayFormatter(type: string): CustomDisplayFormatter | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Get filter type for a custom field type (registered via registerFieldType with filterType)
|
|
111
|
+
*/
|
|
112
|
+
getFilterType(type: string): CustomFilterType | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Check if a custom field type is filterable
|
|
115
|
+
*/
|
|
116
|
+
isFilterable(type: string): boolean;
|
|
86
117
|
/**
|
|
87
118
|
* Check if type is registered
|
|
88
119
|
*/
|
|
@@ -104,7 +135,7 @@ declare class FieldRegistry {
|
|
|
104
135
|
*/
|
|
105
136
|
export declare function getFieldRegistry(): FieldRegistry;
|
|
106
137
|
/**
|
|
107
|
-
* Register a custom field type (
|
|
138
|
+
* Register a custom field type (form, optional display, optional filter).
|
|
108
139
|
* Schema must be defined in entity.validation.schema
|
|
109
140
|
*
|
|
110
141
|
* @example
|
|
@@ -113,41 +144,37 @@ export declare function getFieldRegistry(): FieldRegistry;
|
|
|
113
144
|
* import type { ControlledFieldProps } from '@donotdev/crud';
|
|
114
145
|
* import * as v from 'valibot';
|
|
115
146
|
*
|
|
116
|
-
* // Custom controlled component MUST use framework's useController (not react-hook-form)
|
|
117
147
|
* function ControlledRepairOperationsField({
|
|
118
148
|
* fieldConfig,
|
|
119
149
|
* control,
|
|
120
150
|
* errors,
|
|
121
151
|
* t,
|
|
122
152
|
* }: ControlledFieldProps) {
|
|
123
|
-
* // REQUIRED: Use framework's useController - ensures type compatibility
|
|
124
153
|
* const { field, fieldState } = useController({
|
|
125
154
|
* name: fieldConfig.name,
|
|
126
155
|
* control: control,
|
|
127
156
|
* });
|
|
128
|
-
*
|
|
129
|
-
* // Use field.value and field.onChange
|
|
130
157
|
* return (
|
|
131
158
|
* <div>
|
|
132
159
|
* Your custom UI here
|
|
160
|
+
* {fieldState?.error && <span>{fieldState.error.message}</span>}
|
|
133
161
|
* </div>
|
|
134
162
|
* );
|
|
135
163
|
* }
|
|
136
164
|
*
|
|
137
|
-
* // Register UI component
|
|
138
165
|
* registerFieldType({
|
|
139
166
|
* type: 'repairOperations',
|
|
140
167
|
* controlledComponent: ControlledRepairOperationsField,
|
|
168
|
+
* displayFormatter: (value) => (value != null ? String(value) : '—'),
|
|
169
|
+
* filterable: true,
|
|
170
|
+
* filterType: 'text',
|
|
141
171
|
* });
|
|
142
172
|
*
|
|
143
|
-
* // Then define schema in entity
|
|
144
173
|
* const carEntity = defineEntity({
|
|
145
174
|
* fields: {
|
|
146
175
|
* repairs: {
|
|
147
176
|
* type: 'repairOperations',
|
|
148
|
-
* validation: {
|
|
149
|
-
* schema: v.array(v.object({ operation: v.string(), cost: v.number() }))
|
|
150
|
-
* }
|
|
177
|
+
* validation: { schema: v.array(v.object({ operation: v.string(), cost: v.number() })) }
|
|
151
178
|
* }
|
|
152
179
|
* }
|
|
153
180
|
* });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldRegistry.d.ts","sourceRoot":"","sources":["../src/FieldRegistry.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"FieldRegistry.d.ts","sourceRoot":"","sources":["../src/FieldRegistry.ts"],"names":[],"mappings":"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"}
|
package/dist/FieldRegistry.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";class
|
|
1
|
+
"use client";class d{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:i,filterable:o,filterType:l}=e;this.components.set(t,{controlled:n,uncontrolled:p}),i&&this.displayFormatters.set(t,i),(o!==void 0||l!==void 0)&&this.filterMetadata.set(t,{filterable:o??!1,filterType:l})}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 s=null;function a(){return s||(s=new d),s}function c(r){a().register(r)}function f(r){return a().has(r)}export{a as getFieldRegistry,f as isFieldTypeRegistered,c 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:
|
|
13
|
-
value:
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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<
|
|
47
|
-
|
|
48
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"FunctionsAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/FunctionsAdapter.ts"],"names":[],"mappings":"AA0BA,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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import"valibot";import{hasProvider as p,getProvider as f,wrapCrudError as o,validateWithSchema as c}from"@donotdev/core";import{LIST_SCHEMA_TYPE as u}from"@donotdev/core";let l=null;class y{serverSideOnly=!0;async getCallable(){if(p("callable"))return f("callable");if(!l){const{FirebaseCallableProvider:t}=await import("@donotdev/firebase");l=new t}return l}async getCallableFn(t){const a=await this.getCallable();return async n=>({data:await a.call(t,n)})}async get(t,a,n){const i=`get_${t}`,d=await this.getCallableFn(i);try{const s=(await d({id:a})).data;return n?c(n,s,"FunctionsAdapter.get"):s}catch(r){throw o(r,"FunctionsAdapter","get",t,a)}}async set(t,a,n,i){const d=c(i,n,"FunctionsAdapter.set"),r=`update_${t}`,s=await this.getCallableFn(r);try{await s({id:a,payload:d})}catch(e){throw o(e,"FunctionsAdapter","set",t,a)}}async update(t,a,n){const i=`update_${t}`,d=await this.getCallableFn(i);try{await d({id:a,payload:n})}catch(r){throw o(r,"FunctionsAdapter","update",t,a)}}async delete(t,a){const n=`delete_${t}`,i=await this.getCallableFn(n);try{await i({id:a})}catch(d){throw o(d,"FunctionsAdapter","delete",t,a)}}async add(t,a,n){const i=c(n,a,"FunctionsAdapter.add"),d=`create_${t}`,r=await this.getCallableFn(d);try{const s=await r({payload:i});if(!s.data)throw o(new Error("Create function returned no data"),"FunctionsAdapter","add",t);return{id:s.data.id,data:s.data}}catch(s){throw o(s,"FunctionsAdapter","add",t)}}async query(t,a,n,i=u.LIST){const d=i===u.LIST_CARD?`listCard_${t}`:`list_${t}`,r={};a.where&&(r.where=a.where.map(e=>[e.field,e.operator,e.value])),a.orderBy&&(r.orderBy=a.orderBy.map(e=>[e.field,e.direction||"asc"])),a.limit&&(r.limit=a.limit),a.startAfter&&(r.startAfterId=a.startAfter);const s=await this.getCallableFn(d);try{const e=await s(r);if(!e.data)throw o(new Error("Query function returned no data"),"FunctionsAdapter","query",t);return{items:e.data.items,total:e.data.count,hasMore:e.data.hasMore,lastVisible:e.data.lastVisible}}catch(e){throw o(e,"FunctionsAdapter","query",t)}}subscribe(){return()=>{}}subscribeToCollection(){return()=>{}}}export{y as FunctionsAdapter};
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAGH,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/adapters/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{FunctionsAdapter as t}from"./FunctionsAdapter";export{t as FunctionsAdapter};
|