@fayz-ai/core 0.1.6

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 (172) hide show
  1. package/dist/app/render.d.ts +16 -0
  2. package/dist/app/render.d.ts.map +1 -0
  3. package/dist/blocks/index.d.ts +25 -0
  4. package/dist/blocks/index.d.ts.map +1 -0
  5. package/dist/chunk-2NTHNWEH.cjs +32 -0
  6. package/dist/chunk-2NTHNWEH.cjs.map +1 -0
  7. package/dist/chunk-5EAZOMVP.js +1609 -0
  8. package/dist/chunk-5EAZOMVP.js.map +1 -0
  9. package/dist/chunk-5TKIZVVJ.cjs +16 -0
  10. package/dist/chunk-5TKIZVVJ.cjs.map +1 -0
  11. package/dist/chunk-7RTUHSKY.js +3 -0
  12. package/dist/chunk-7RTUHSKY.js.map +1 -0
  13. package/dist/chunk-AXTWKF5X.cjs +38 -0
  14. package/dist/chunk-AXTWKF5X.cjs.map +1 -0
  15. package/dist/chunk-EGWWQ7AT.cjs +355 -0
  16. package/dist/chunk-EGWWQ7AT.cjs.map +1 -0
  17. package/dist/chunk-KAYQFSR6.js +298 -0
  18. package/dist/chunk-KAYQFSR6.js.map +1 -0
  19. package/dist/chunk-LSVBDYFQ.cjs +1639 -0
  20. package/dist/chunk-LSVBDYFQ.cjs.map +1 -0
  21. package/dist/chunk-SNQW6TRF.cjs +658 -0
  22. package/dist/chunk-SNQW6TRF.cjs.map +1 -0
  23. package/dist/chunk-WKZVSKEU.js +26 -0
  24. package/dist/chunk-WKZVSKEU.js.map +1 -0
  25. package/dist/chunk-Y2C2FBQP.js +35 -0
  26. package/dist/chunk-Y2C2FBQP.js.map +1 -0
  27. package/dist/chunk-YSBXGHAM.js +643 -0
  28. package/dist/chunk-YSBXGHAM.js.map +1 -0
  29. package/dist/data/archetype.d.ts +13 -0
  30. package/dist/data/archetype.d.ts.map +1 -0
  31. package/dist/data/cached.d.ts +19 -0
  32. package/dist/data/cached.d.ts.map +1 -0
  33. package/dist/data/fayz-api.d.ts +19 -0
  34. package/dist/data/fayz-api.d.ts.map +1 -0
  35. package/dist/data/index.cjs +32 -0
  36. package/dist/data/index.cjs.map +1 -0
  37. package/dist/data/index.d.ts +9 -0
  38. package/dist/data/index.d.ts.map +1 -0
  39. package/dist/data/index.js +3 -0
  40. package/dist/data/index.js.map +1 -0
  41. package/dist/data/mock.d.ts +7 -0
  42. package/dist/data/mock.d.ts.map +1 -0
  43. package/dist/data/resolve.d.ts +22 -0
  44. package/dist/data/resolve.d.ts.map +1 -0
  45. package/dist/data/supabase.d.ts +18 -0
  46. package/dist/data/supabase.d.ts.map +1 -0
  47. package/dist/data/types.d.ts +23 -0
  48. package/dist/data/types.d.ts.map +1 -0
  49. package/dist/entity/index.cjs +28 -0
  50. package/dist/entity/index.cjs.map +1 -0
  51. package/dist/entity/index.d.ts +3 -0
  52. package/dist/entity/index.d.ts.map +1 -0
  53. package/dist/entity/index.js +3 -0
  54. package/dist/entity/index.js.map +1 -0
  55. package/dist/entity/registry.d.ts +20 -0
  56. package/dist/entity/registry.d.ts.map +1 -0
  57. package/dist/events/index.d.ts +16 -0
  58. package/dist/events/index.d.ts.map +1 -0
  59. package/dist/i18n/index.cjs +44 -0
  60. package/dist/i18n/index.cjs.map +1 -0
  61. package/dist/i18n/index.d.ts +21 -0
  62. package/dist/i18n/index.d.ts.map +1 -0
  63. package/dist/i18n/index.js +3 -0
  64. package/dist/i18n/index.js.map +1 -0
  65. package/dist/i18n/shell-translations.d.ts +2 -0
  66. package/dist/i18n/shell-translations.d.ts.map +1 -0
  67. package/dist/index.cjs +911 -0
  68. package/dist/index.cjs.map +1 -0
  69. package/dist/index.d.ts +46 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +596 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/lib/cache.d.ts +21 -0
  74. package/dist/lib/cache.d.ts.map +1 -0
  75. package/dist/lib/csv.d.ts +14 -0
  76. package/dist/lib/csv.d.ts.map +1 -0
  77. package/dist/lib/format.d.ts +7 -0
  78. package/dist/lib/format.d.ts.map +1 -0
  79. package/dist/manifest/index.d.ts +80 -0
  80. package/dist/manifest/index.d.ts.map +1 -0
  81. package/dist/plugin/createSafeDataProvider.d.ts +14 -0
  82. package/dist/plugin/createSafeDataProvider.d.ts.map +1 -0
  83. package/dist/plugin/index.cjs +32 -0
  84. package/dist/plugin/index.cjs.map +1 -0
  85. package/dist/plugin/index.d.ts +2 -0
  86. package/dist/plugin/index.d.ts.map +1 -0
  87. package/dist/plugin/index.js +3 -0
  88. package/dist/plugin/index.js.map +1 -0
  89. package/dist/plugin/runtime.d.ts +31 -0
  90. package/dist/plugin/runtime.d.ts.map +1 -0
  91. package/dist/registry/index.d.ts +88 -0
  92. package/dist/registry/index.d.ts.map +1 -0
  93. package/dist/router/index.cjs +16 -0
  94. package/dist/router/index.cjs.map +1 -0
  95. package/dist/router/index.d.ts +8 -0
  96. package/dist/router/index.d.ts.map +1 -0
  97. package/dist/router/index.js +3 -0
  98. package/dist/router/index.js.map +1 -0
  99. package/dist/runtime/index.cjs +16 -0
  100. package/dist/runtime/index.cjs.map +1 -0
  101. package/dist/runtime/index.d.ts +3 -0
  102. package/dist/runtime/index.d.ts.map +1 -0
  103. package/dist/runtime/index.js +3 -0
  104. package/dist/runtime/index.js.map +1 -0
  105. package/dist/runtime/oauth.d.ts +96 -0
  106. package/dist/runtime/oauth.d.ts.map +1 -0
  107. package/dist/tenant/index.d.ts +3 -0
  108. package/dist/tenant/index.d.ts.map +1 -0
  109. package/dist/types/auth.d.ts +27 -0
  110. package/dist/types/auth.d.ts.map +1 -0
  111. package/dist/types/billing.d.ts +17 -0
  112. package/dist/types/billing.d.ts.map +1 -0
  113. package/dist/types/crud.d.ts +107 -0
  114. package/dist/types/crud.d.ts.map +1 -0
  115. package/dist/types/entities.d.ts +134 -0
  116. package/dist/types/entities.d.ts.map +1 -0
  117. package/dist/types/i18n.d.ts +8 -0
  118. package/dist/types/i18n.d.ts.map +1 -0
  119. package/dist/types/index.cjs +4 -0
  120. package/dist/types/index.cjs.map +1 -0
  121. package/dist/types/index.d.ts +10 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +3 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/types/org.d.ts +96 -0
  126. package/dist/types/org.d.ts.map +1 -0
  127. package/dist/types/permissions.d.ts +25 -0
  128. package/dist/types/permissions.d.ts.map +1 -0
  129. package/dist/types/plugins.d.ts +314 -0
  130. package/dist/types/plugins.d.ts.map +1 -0
  131. package/dist/types/theme.d.ts +16 -0
  132. package/dist/types/theme.d.ts.map +1 -0
  133. package/package.json +98 -0
  134. package/src/app/render.tsx +69 -0
  135. package/src/blocks/index.tsx +68 -0
  136. package/src/data/archetype.ts +210 -0
  137. package/src/data/cached.ts +55 -0
  138. package/src/data/fayz-api.ts +200 -0
  139. package/src/data/index.ts +8 -0
  140. package/src/data/mock.ts +84 -0
  141. package/src/data/resolve.ts +108 -0
  142. package/src/data/supabase.ts +169 -0
  143. package/src/data/types.ts +20 -0
  144. package/src/entity/index.ts +2 -0
  145. package/src/entity/registry.ts +42 -0
  146. package/src/events/index.tsx +82 -0
  147. package/src/i18n/index.ts +250 -0
  148. package/src/i18n/shell-translations.ts +1391 -0
  149. package/src/index.ts +132 -0
  150. package/src/lib/cache.ts +118 -0
  151. package/src/lib/csv.ts +54 -0
  152. package/src/lib/format.ts +58 -0
  153. package/src/manifest/app-manifest.schema.json +103 -0
  154. package/src/manifest/index.ts +394 -0
  155. package/src/plugin/createSafeDataProvider.ts +30 -0
  156. package/src/plugin/index.ts +1 -0
  157. package/src/plugin/runtime.ts +313 -0
  158. package/src/registry/index.ts +163 -0
  159. package/src/router/index.ts +29 -0
  160. package/src/runtime/index.ts +15 -0
  161. package/src/runtime/oauth.ts +219 -0
  162. package/src/tenant/index.ts +18 -0
  163. package/src/types/auth.ts +26 -0
  164. package/src/types/billing.ts +17 -0
  165. package/src/types/crud.ts +108 -0
  166. package/src/types/entities.ts +142 -0
  167. package/src/types/i18n.ts +7 -0
  168. package/src/types/index.ts +10 -0
  169. package/src/types/org.ts +94 -0
  170. package/src/types/permissions.ts +32 -0
  171. package/src/types/plugins.ts +363 -0
  172. package/src/types/theme.ts +17 -0
@@ -0,0 +1,210 @@
1
+ import type { DataProvider, CrudQuery, CrudResult } from './types'
2
+ import { getSupabaseClientOptional } from './supabase'
3
+ import type { EntityArchetype } from '../types/entities'
4
+
5
+ // Archetypes that have a `kind` discriminator column
6
+ const HAS_KIND = new Set<EntityArchetype>(['person', 'category', 'order', 'transaction', 'schedule', 'location'])
7
+
8
+ const ARCHETYPE_CONFIG: Record<EntityArchetype, { table: string; fkColumn: string }> = {
9
+ person: { table: 'persons', fkColumn: 'person_id' },
10
+ category: { table: 'categories', fkColumn: 'category_id' },
11
+ product: { table: 'products', fkColumn: 'product_id' },
12
+ service: { table: 'services', fkColumn: 'service_id' },
13
+ location: { table: 'locations', fkColumn: 'location_id' },
14
+ order: { table: 'orders', fkColumn: 'order_id' },
15
+ transaction: { table: 'transactions', fkColumn: 'transaction_id' },
16
+
17
+ schedule: { table: 'schedules', fkColumn: 'schedule_id' },
18
+ }
19
+
20
+ const ARCHETYPE_COLUMNS: Record<EntityArchetype, Set<string>> = {
21
+ person: new Set(['name', 'email', 'phone', 'document_number', 'avatar_url', 'date_of_birth', 'address', 'city', 'state', 'country', 'postal_code', 'tags', 'is_active', 'notes', 'metadata']),
22
+ category: new Set(['name', 'slug', 'parent_id', 'icon', 'color', 'sort_order', 'is_active', 'metadata']),
23
+ product: new Set(['name', 'description', 'sku', 'price', 'cost', 'currency', 'unit', 'image_url', 'stock', 'min_stock', 'status', 'is_active', 'tags', 'metadata', 'category_id']),
24
+ service: new Set(['name', 'description', 'price', 'cost', 'currency', 'duration_minutes', 'image_url', 'status', 'is_active', 'tags', 'metadata', 'category_id']),
25
+ location: new Set(['name', 'email', 'phone', 'address', 'city', 'state', 'country', 'postal_code', 'is_headquarters', 'is_active', 'tags', 'notes', 'metadata']),
26
+ order: new Set(['reference_number', 'status', 'party_id', 'assignee_id', 'location_id', 'subtotal', 'discount', 'tax', 'total', 'currency', 'due_at', 'completed_at', 'notes', 'tags', 'metadata']),
27
+ transaction: new Set(['order_id', 'party_id', 'amount', 'currency', 'payment_method', 'reference', 'status', 'transacted_at', 'notes', 'metadata']),
28
+
29
+ schedule: new Set(['assignee_id', 'location_id', 'day_of_week', 'specific_date', 'starts_at', 'ends_at', 'is_active', 'metadata']),
30
+ }
31
+
32
+ function camelToSnake(str: string): string {
33
+ return str.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase())
34
+ }
35
+
36
+ function snakeToCamel(str: string): string {
37
+ return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
38
+ }
39
+
40
+ function mapRow<T>(row: Record<string, unknown>): T {
41
+ const result: Record<string, unknown> = {}
42
+ for (const [key, value] of Object.entries(row)) {
43
+ result[snakeToCamel(key)] = value
44
+ }
45
+ return result as T
46
+ }
47
+
48
+ export interface ArchetypeProviderConfig {
49
+ archetype: EntityArchetype
50
+ archetypeKind: string
51
+ projectTable: string
52
+ tenantId: () => string | undefined
53
+ searchColumns?: string[]
54
+ }
55
+
56
+ export function createArchetypeProvider<T extends { id: string }>(
57
+ config: ArchetypeProviderConfig,
58
+ ): DataProvider<T> {
59
+ const ac = ARCHETYPE_CONFIG[config.archetype]
60
+ const archetypeColumns = ARCHETYPE_COLUMNS[config.archetype]
61
+ const { fkColumn } = ac
62
+
63
+ function getClient() {
64
+ const supabase = getSupabaseClientOptional() as any
65
+ if (!supabase) throw new Error('Supabase client not available')
66
+ return supabase
67
+ }
68
+
69
+ function coreClient() {
70
+ return getClient().schema('saas_core')
71
+ }
72
+
73
+ function splitFields(data: Record<string, unknown>): { archetypeData: Record<string, unknown>; projectData: Record<string, unknown> } {
74
+ const archetypeData: Record<string, unknown> = {}
75
+ const projectData: Record<string, unknown> = {}
76
+ for (const [key, value] of Object.entries(data)) {
77
+ if (['id', 'createdAt', 'updatedAt', 'tenantId', fkColumn].includes(key)) continue
78
+ const snakeKey = camelToSnake(key)
79
+ if (archetypeColumns.has(snakeKey)) {
80
+ archetypeData[snakeKey] = value
81
+ } else {
82
+ projectData[snakeKey] = value
83
+ }
84
+ }
85
+ return { archetypeData, projectData }
86
+ }
87
+
88
+ // View name convention: v_{shortName} — maps project table to its view
89
+ const VIEW_MAP: Record<string, string> = {
90
+ clients: 'v_clients',
91
+ staff_members: 'v_staff',
92
+ }
93
+ const viewName = VIEW_MAP[config.projectTable] ?? `v_${config.projectTable}`
94
+
95
+ return {
96
+ async list(query: CrudQuery): Promise<CrudResult<T>> {
97
+ const tenantId = config.tenantId()
98
+ if (!tenantId) return { data: [], total: 0 }
99
+
100
+ // Single query via v_ view (JOINs public table with saas_core archetype)
101
+ let q = getClient()
102
+ .from(viewName)
103
+ .select('*', { count: 'exact' })
104
+ .eq('tenant_id', tenantId)
105
+
106
+ // Search
107
+ if (query.search && config.searchColumns && config.searchColumns.length > 0) {
108
+ const term = `%${query.search}%`
109
+ const orClauses = config.searchColumns
110
+ .map((col) => camelToSnake(col))
111
+ .map((col) => `${col}.ilike.${term}`)
112
+ if (orClauses.length > 0) {
113
+ q = q.or(orClauses.join(','))
114
+ }
115
+ }
116
+
117
+ // Sort
118
+ if (query.sortBy) {
119
+ q = q.order(camelToSnake(query.sortBy), { ascending: query.sortDir !== 'desc' })
120
+ } else {
121
+ q = q.order('created_at', { ascending: false })
122
+ }
123
+
124
+ // Pagination
125
+ const page = query.page ?? 1
126
+ const pageSize = query.pageSize ?? 50
127
+ const from = (page - 1) * pageSize
128
+ q = q.range(from, from + pageSize - 1)
129
+
130
+ const { data, error, count } = await q
131
+ if (error) throw error
132
+
133
+ const rows = (data ?? []).map((row: Record<string, unknown>) => mapRow<T>(row))
134
+ return { data: rows, total: count ?? 0 }
135
+ },
136
+
137
+ async create(data) {
138
+ const tenantId = config.tenantId()
139
+ if (!tenantId) throw new Error('No tenant selected')
140
+
141
+ const { archetypeData, projectData } = splitFields(data as Record<string, unknown>)
142
+
143
+ // 1. Insert into archetype table
144
+ archetypeData.tenant_id = tenantId
145
+ if (HAS_KIND.has(config.archetype)) {
146
+ archetypeData.kind = config.archetypeKind
147
+ }
148
+
149
+ const { data: archetypeRow, error: archetypeError } = await coreClient()
150
+ .from(ac.table)
151
+ .insert(archetypeData)
152
+ .select()
153
+ .single()
154
+
155
+ if (archetypeError) throw archetypeError
156
+ const archetypeId = (archetypeRow as any).id
157
+
158
+ // 2. Insert into project table with archetype FK as PK
159
+ projectData[fkColumn] = archetypeId
160
+ projectData.tenant_id = tenantId
161
+
162
+ const { error: projectError } = await getClient()
163
+ .from(config.projectTable)
164
+ .insert(projectData)
165
+
166
+ if (projectError) {
167
+ await coreClient().from(ac.table).delete().eq('id', archetypeId)
168
+ throw projectError
169
+ }
170
+
171
+ return { id: archetypeId, ...archetypeRow, ...projectData } as unknown as T
172
+ },
173
+
174
+ async update(id, data) {
175
+ const { archetypeData, projectData } = splitFields(data as Record<string, unknown>)
176
+
177
+ if (Object.keys(archetypeData).length > 0) {
178
+ const { error } = await coreClient()
179
+ .from(ac.table)
180
+ .update(archetypeData)
181
+ .eq('id', id)
182
+ if (error) throw error
183
+ }
184
+
185
+ if (Object.keys(projectData).length > 0) {
186
+ const { error } = await getClient()
187
+ .from(config.projectTable)
188
+ .update(projectData)
189
+ .eq(fkColumn, id)
190
+ if (error) throw error
191
+ }
192
+
193
+ // Re-fetch
194
+ const { data: archetypeRow } = await coreClient().from(ac.table).select('*').eq('id', id).single()
195
+ const { data: projectRow } = await getClient().from(config.projectTable).select('*').eq(fkColumn, id).single()
196
+
197
+ const flat = { ...archetypeRow, ...projectRow, id }
198
+ delete (flat as any)[fkColumn]
199
+ return mapRow<T>(flat as Record<string, unknown>)
200
+ },
201
+
202
+ async remove(id) {
203
+ const { error } = await coreClient()
204
+ .from(ac.table)
205
+ .delete()
206
+ .eq('id', id)
207
+ if (error) throw error
208
+ },
209
+ }
210
+ }
@@ -0,0 +1,55 @@
1
+ import type { DataProvider, CrudQuery, CrudResult } from './types'
2
+ import { globalCache, stableKey } from '../lib/cache'
3
+
4
+ export interface ProviderCacheOptions {
5
+ /** Table/entity name — used as cache key namespace */
6
+ table: string
7
+ /** Dynamic tenant ID resolver */
8
+ tenantId: () => string | undefined
9
+ /** Override default TTL for this entity (ms) */
10
+ ttl?: number
11
+ }
12
+
13
+ /**
14
+ * Wraps a DataProvider with transparent read caching + mutation invalidation.
15
+ *
16
+ * - `list()` results are cached by query params
17
+ * - `create()`, `update()`, `remove()` invalidate all cached entries for the table
18
+ */
19
+ export function withCache<T extends { id: string }>(
20
+ provider: DataProvider<T>,
21
+ options: ProviderCacheOptions,
22
+ ): DataProvider<T> {
23
+ function prefix(): string {
24
+ return `${options.tenantId() ?? '_'}:${options.table}`
25
+ }
26
+
27
+ return {
28
+ async list(query: CrudQuery): Promise<CrudResult<T>> {
29
+ const key = `${prefix()}:${stableKey(query)}`
30
+ const cached = globalCache.get<CrudResult<T>>(key)
31
+ if (cached) return cached
32
+
33
+ const result = await provider.list(query)
34
+ globalCache.set(key, result, options.ttl)
35
+ return result
36
+ },
37
+
38
+ async create(data) {
39
+ const result = await provider.create(data)
40
+ globalCache.invalidate(prefix())
41
+ return result
42
+ },
43
+
44
+ async update(id, data) {
45
+ const result = await provider.update(id, data)
46
+ globalCache.invalidate(prefix())
47
+ return result
48
+ },
49
+
50
+ async remove(id) {
51
+ await provider.remove(id)
52
+ globalCache.invalidate(prefix())
53
+ },
54
+ }
55
+ }
@@ -0,0 +1,200 @@
1
+ import type { CrudQuery, CrudResult, DataProvider } from './types'
2
+
3
+ type FayzApiFilterOperator = 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'like' | 'ilike' | 'is_null' | 'is_not_null'
4
+
5
+ interface FayzApiTableFilter {
6
+ column: string
7
+ operator: FayzApiFilterOperator
8
+ value?: unknown
9
+ }
10
+
11
+ export interface FayzApiProviderConfig {
12
+ baseUrl?: string
13
+ projectId?: string
14
+ tenantId?: string | (() => string | undefined)
15
+ tenantIdColumn?: string | false
16
+ entityKey?: string
17
+ table?: string
18
+ schema?: string
19
+ idColumn?: string
20
+ searchColumns?: string[]
21
+ runtimeToken?: string | (() => string | undefined | Promise<string | undefined>)
22
+ headers?: () => HeadersInit | Promise<HeadersInit>
23
+ fetcher?: typeof fetch
24
+ }
25
+
26
+ function resolveTenantId(config?: FayzApiProviderConfig): string | undefined {
27
+ const tenantId = config?.tenantId
28
+ return typeof tenantId === 'function' ? tenantId() : tenantId
29
+ }
30
+
31
+ async function resolveRuntimeToken(config?: FayzApiProviderConfig): Promise<string | undefined> {
32
+ const runtimeToken = config?.runtimeToken
33
+ return typeof runtimeToken === 'function' ? runtimeToken() : runtimeToken
34
+ }
35
+
36
+ function trimTrailingSlash(value: string): string {
37
+ return value.replace(/\/+$/, '')
38
+ }
39
+
40
+ function appendQuery(
41
+ url: URL,
42
+ query: CrudQuery,
43
+ tenantId?: string,
44
+ tenantIdColumn = 'tenant_id',
45
+ searchColumns: string[] = [],
46
+ ): void {
47
+ const filters = normalizeFilters(query.filters)
48
+ if (query.search && searchColumns[0]) {
49
+ filters.push({ column: searchColumns[0], operator: 'ilike', value: `%${query.search}%` })
50
+ }
51
+ if (query.sortBy) url.searchParams.set('sortColumn', query.sortBy)
52
+ if (query.sortDir) url.searchParams.set('sortDirection', query.sortDir)
53
+ if (query.page != null) url.searchParams.set('page', String(query.page))
54
+ if (query.pageSize != null) url.searchParams.set('limit', String(query.pageSize))
55
+ if (tenantId) filters.push({ column: tenantIdColumn, operator: 'eq', value: tenantId })
56
+ if (filters.length > 0) url.searchParams.set('filters', JSON.stringify(filters))
57
+ }
58
+
59
+ async function parseResponse<T>(response: Response): Promise<T> {
60
+ if (!response.ok) {
61
+ const text = await response.text().catch(() => '')
62
+ throw new Error(`[@fayz-ai/core] Fayz API request failed (${response.status}): ${text || response.statusText}`)
63
+ }
64
+ return response.json() as Promise<T>
65
+ }
66
+
67
+ function isFilterOperator(value: unknown): value is FayzApiFilterOperator {
68
+ return typeof value === 'string' && [
69
+ 'eq',
70
+ 'neq',
71
+ 'gt',
72
+ 'lt',
73
+ 'gte',
74
+ 'lte',
75
+ 'like',
76
+ 'ilike',
77
+ 'is_null',
78
+ 'is_not_null',
79
+ ].includes(value)
80
+ }
81
+
82
+ function isRecord(value: unknown): value is Record<string, unknown> {
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
84
+ }
85
+
86
+ function normalizeFilters(filters?: Record<string, unknown>): FayzApiTableFilter[] {
87
+ if (!filters) return []
88
+ return Object.entries(filters).map(([column, raw]) => {
89
+ if (isRecord(raw) && isFilterOperator(raw.operator)) {
90
+ return { column, operator: raw.operator, value: raw.value }
91
+ }
92
+ return { column, operator: 'eq', value: raw }
93
+ })
94
+ }
95
+
96
+ export function createFayzApiProvider<T extends { id: string }>(
97
+ entityOrTable: string,
98
+ config: FayzApiProviderConfig = {},
99
+ ): DataProvider<T> {
100
+ const fetcher = config.fetcher ?? fetch
101
+ const baseUrl = trimTrailingSlash(config.baseUrl ?? '')
102
+ const projectId = config.projectId
103
+ const table = config.table ?? entityOrTable
104
+ const idColumn = config.idColumn ?? 'id'
105
+
106
+ async function headers(runtimeToken?: string): Promise<HeadersInit> {
107
+ return {
108
+ 'Content-Type': 'application/json',
109
+ ...(config.headers ? await config.headers() : {}),
110
+ ...(runtimeToken ? { Authorization: `Bearer ${runtimeToken}` } : {}),
111
+ }
112
+ }
113
+
114
+ function tableRowsUrl(runtime = false): URL {
115
+ const prefix = baseUrl || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost')
116
+ const encodedProjectId = encodeURIComponent(projectId ?? 'current')
117
+ const encodedTable = encodeURIComponent(table)
118
+ const route = runtime
119
+ ? `/api/v1/runtime/projects/${encodedProjectId}/database/tables/${encodedTable}/rows`
120
+ : `/api/projects/${encodedProjectId}/database/tables/${encodedTable}/rows`
121
+ const url = new URL(route, prefix)
122
+ if (config.schema) url.searchParams.set('schema', config.schema)
123
+ return url
124
+ }
125
+
126
+ function withTenant(data: Record<string, unknown>, runtime = false): Record<string, unknown> {
127
+ if (runtime) return data
128
+ const tenantId = resolveTenantId(config)
129
+ if (!tenantId || config.tenantIdColumn === false) return data
130
+ return { ...data, [config.tenantIdColumn ?? 'tenant_id']: tenantId }
131
+ }
132
+
133
+ function primaryKeys(id: string): Record<string, unknown> {
134
+ return { [idColumn]: id }
135
+ }
136
+
137
+ async function listRows(query: CrudQuery): Promise<CrudResult<T>> {
138
+ const runtimeToken = await resolveRuntimeToken(config)
139
+ const runtime = Boolean(runtimeToken)
140
+ const url = tableRowsUrl(runtime)
141
+ appendQuery(
142
+ url,
143
+ query,
144
+ runtime || config.tenantIdColumn === false ? undefined : resolveTenantId(config),
145
+ config.tenantIdColumn || undefined,
146
+ config.searchColumns,
147
+ )
148
+ const result = await parseResponse<{ rows: T[]; total: number }>(await fetcher(url, { headers: await headers(runtimeToken) }))
149
+ return { data: result.rows, total: result.total }
150
+ }
151
+
152
+ async function assertTenantScopedRowExists(id: string, runtime = false): Promise<void> {
153
+ if (runtime) return
154
+ const tenantId = config.tenantIdColumn === false ? undefined : resolveTenantId(config)
155
+ if (!tenantId) return
156
+
157
+ const result = await listRows({ filters: { [idColumn]: id }, page: 1, pageSize: 1 })
158
+ if (result.data.length === 0) {
159
+ throw new Error(`[@fayz-ai/core] Row "${id}" was not found for the active tenant.`)
160
+ }
161
+ }
162
+
163
+ return {
164
+ async list(query: CrudQuery): Promise<CrudResult<T>> {
165
+ return listRows(query)
166
+ },
167
+
168
+ async create(data) {
169
+ const runtimeToken = await resolveRuntimeToken(config)
170
+ const runtime = Boolean(runtimeToken)
171
+ return parseResponse<T>(await fetcher(tableRowsUrl(runtime), {
172
+ method: 'POST',
173
+ headers: await headers(runtimeToken),
174
+ body: JSON.stringify(withTenant(data as Record<string, unknown>, runtime)),
175
+ }))
176
+ },
177
+
178
+ async update(id, data) {
179
+ const runtimeToken = await resolveRuntimeToken(config)
180
+ const runtime = Boolean(runtimeToken)
181
+ await assertTenantScopedRowExists(id, runtime)
182
+ return parseResponse<T>(await fetcher(tableRowsUrl(runtime), {
183
+ method: 'PATCH',
184
+ headers: await headers(runtimeToken),
185
+ body: JSON.stringify({ primaryKeys: primaryKeys(id), data }),
186
+ }))
187
+ },
188
+
189
+ async remove(id) {
190
+ const runtimeToken = await resolveRuntimeToken(config)
191
+ const runtime = Boolean(runtimeToken)
192
+ await assertTenantScopedRowExists(id, runtime)
193
+ await parseResponse<{ deletedCount: number }>(await fetcher(tableRowsUrl(runtime), {
194
+ method: 'DELETE',
195
+ headers: await headers(runtimeToken),
196
+ body: JSON.stringify({ rows: [primaryKeys(id)] }),
197
+ }))
198
+ },
199
+ }
200
+ }
@@ -0,0 +1,8 @@
1
+ export { createSupabaseProvider, setGlobalSupabaseClient, getSupabaseClientOptional } from './supabase'
2
+ export { createFayzApiProvider } from './fayz-api'
3
+ export { createMockProvider } from './mock'
4
+ export { resolveDataProvider } from './resolve'
5
+ export type { DataProvider, CrudQuery, CrudResult } from './types'
6
+ export type { SupabaseProviderConfig } from './supabase'
7
+ export type { FayzApiProviderConfig } from './fayz-api'
8
+ export type { ResolveDataProviderOptions } from './resolve'
@@ -0,0 +1,84 @@
1
+ import type { DataProvider, CrudQuery, CrudResult } from './types'
2
+ import type { EntityDef } from '../types/crud'
3
+
4
+ export function createMockProvider<T extends { id: string; [key: string]: unknown }>(
5
+ entityDefOrSearchKeys: EntityDef<T> | string[],
6
+ initialData: T[] = [],
7
+ ): DataProvider<T> {
8
+ let items = [...initialData]
9
+
10
+ const searchableKeys = Array.isArray(entityDefOrSearchKeys)
11
+ ? entityDefOrSearchKeys
12
+ : entityDefOrSearchKeys.fields.filter((f) => f.searchable).map((f) => f.key)
13
+
14
+ const defaultSort = Array.isArray(entityDefOrSearchKeys) ? undefined : entityDefOrSearchKeys.defaultSort
15
+ const defaultSortDir = Array.isArray(entityDefOrSearchKeys) ? 'asc' : (entityDefOrSearchKeys.defaultSortDir ?? 'asc')
16
+
17
+ return {
18
+ async list(query: CrudQuery): Promise<CrudResult<T>> {
19
+ let filtered = [...items]
20
+
21
+ if (query.search && searchableKeys.length > 0) {
22
+ const term = query.search.toLowerCase()
23
+ filtered = filtered.filter((item) =>
24
+ searchableKeys.some((key) => {
25
+ const val = item[key]
26
+ return typeof val === 'string' && val.toLowerCase().includes(term)
27
+ }),
28
+ )
29
+ }
30
+
31
+ // Faceted / dynamic filters (equality match per key).
32
+ if (query.filters) {
33
+ for (const [key, val] of Object.entries(query.filters)) {
34
+ if (val == null || val === '') continue
35
+ filtered = filtered.filter((item) => item[key] === val)
36
+ }
37
+ }
38
+
39
+ const sortBy = query.sortBy ?? defaultSort
40
+ const sortDir = query.sortDir ?? defaultSortDir
41
+ if (sortBy) {
42
+ filtered.sort((a, b) => {
43
+ const av = a[sortBy] ?? ''
44
+ const bv = b[sortBy] ?? ''
45
+ const cmp = typeof av === 'number' && typeof bv === 'number'
46
+ ? av - bv
47
+ : String(av).localeCompare(String(bv))
48
+ return sortDir === 'desc' ? -cmp : cmp
49
+ })
50
+ }
51
+
52
+ const total = filtered.length
53
+ const page = query.page ?? 1
54
+ const pageSize = query.pageSize ?? 50
55
+ const start = (page - 1) * pageSize
56
+ const data = filtered.slice(start, start + pageSize)
57
+ return { data, total }
58
+ },
59
+
60
+ async create(data) {
61
+ const now = new Date().toISOString()
62
+ const item = {
63
+ ...data,
64
+ id: crypto.randomUUID(),
65
+ tenantId: 'mock-tenant',
66
+ createdAt: now,
67
+ updatedAt: now,
68
+ } as unknown as T
69
+ items.unshift(item)
70
+ return item
71
+ },
72
+
73
+ async update(id, data) {
74
+ const idx = items.findIndex((i) => i.id === id)
75
+ if (idx === -1) throw new Error(`[@fayz-ai/core] Mock provider: item not found: ${id}`)
76
+ items[idx] = { ...items[idx], ...data, updatedAt: new Date().toISOString() }
77
+ return items[idx]!
78
+ },
79
+
80
+ async remove(id) {
81
+ items = items.filter((i) => i.id !== id)
82
+ },
83
+ }
84
+ }
@@ -0,0 +1,108 @@
1
+ import type { EntityDef } from '../types/crud'
2
+ import type { BackendRef } from '../manifest'
3
+ import type { DataProvider } from './types'
4
+ import { getSupabaseClientOptional } from './supabase'
5
+ import { createSupabaseProvider } from './supabase'
6
+ import { createArchetypeProvider } from './archetype'
7
+ import { createMockProvider } from './mock'
8
+ import { createFayzApiProvider, type FayzApiProviderConfig } from './fayz-api'
9
+ import { withCache } from './cached'
10
+ import { getActiveTenantId } from '../tenant'
11
+
12
+ /**
13
+ * Resolve the best data provider for an EntityDef.
14
+ * Supabase → Archetype → Mock (in order of preference).
15
+ *
16
+ * Reusable by createCrudPage, PluginRegistryManager, or any component
17
+ * that needs a DataProvider from an EntityDef.
18
+ */
19
+ export interface ResolveDataProviderOptions {
20
+ backend?: BackendRef
21
+ fayzApi?: Omit<FayzApiProviderConfig, 'entityKey' | 'table'>
22
+ customProviders?: Record<string, <T extends { id: string }>(entityDef: EntityDef<T>, mockData?: T[]) => DataProvider<T>>
23
+ }
24
+
25
+ export function resolveDataProvider<T extends { id: string }>(
26
+ entityDef: EntityDef<T>,
27
+ mockData?: T[],
28
+ options: ResolveDataProviderOptions = {},
29
+ ): DataProvider<T> {
30
+ const backend = options.backend
31
+
32
+ if (backend?.provider === 'mock') {
33
+ return createMockProvider(entityDef, mockData)
34
+ }
35
+
36
+ if (backend?.provider === 'fayz-api') {
37
+ const table = entityDef.data?.table ?? entityDef.name
38
+ return createFayzApiProvider<T>(table, {
39
+ ...options.fayzApi,
40
+ baseUrl: options.fayzApi?.baseUrl ?? backend.url,
41
+ projectId: options.fayzApi?.projectId ?? backend.projectRef,
42
+ entityKey: entityDef.name ?? table,
43
+ table,
44
+ schema: options.fayzApi?.schema ?? entityDef.data?.schema,
45
+ idColumn: options.fayzApi?.idColumn ?? entityDef.data?.columnMap?.id,
46
+ searchColumns: options.fayzApi?.searchColumns ?? entityDef.data?.searchColumns ?? entityDef.fields
47
+ .filter((field) => field.searchable)
48
+ .map((field) => field.key),
49
+ tenantIdColumn: entityDef.data?.tenantScoped === false
50
+ ? false
51
+ : options.fayzApi?.tenantIdColumn ?? entityDef.data?.tenantIdColumn,
52
+ tenantId: options.fayzApi?.tenantId ?? (() => getActiveTenantId()),
53
+ })
54
+ }
55
+
56
+ if (backend?.provider === 'custom') {
57
+ const adapterId = backend.adapterId
58
+ const factory = adapterId ? options.customProviders?.[adapterId] : undefined
59
+ if (!adapterId || !factory) {
60
+ throw new Error(`[@fayz-ai/core] Custom data provider "${adapterId ?? 'unknown'}" is not registered.`)
61
+ }
62
+ return factory<T>(entityDef, mockData)
63
+ }
64
+
65
+ const client = getSupabaseClientOptional()
66
+
67
+ if (client && entityDef.data?.table) {
68
+ const tenantId = () => getActiveTenantId()
69
+ const cacheOptions = {
70
+ table: entityDef.data.table,
71
+ tenantId,
72
+ ttl: entityDef.data.cacheTTL,
73
+ }
74
+
75
+ if (entityDef.data.archetype && entityDef.data.archetypeKind && !entityDef.data.schema) {
76
+ return withCache(createArchetypeProvider<T>({
77
+ archetype: entityDef.data.archetype,
78
+ archetypeKind: entityDef.data.archetypeKind,
79
+ projectTable: entityDef.data.table,
80
+ tenantId,
81
+ searchColumns: entityDef.data.searchColumns ?? entityDef.fields
82
+ .filter((field) => field.searchable)
83
+ .map((field) => field.key),
84
+ }), cacheOptions)
85
+ }
86
+
87
+ return withCache(createSupabaseProvider<T>(entityDef.data.table, {
88
+ schema: entityDef.data.schema,
89
+ tenantId: entityDef.data.tenantScoped === false ? undefined : tenantId,
90
+ tenantIdColumn: entityDef.data.tenantIdColumn,
91
+ searchColumns: entityDef.data.searchColumns ?? entityDef.fields
92
+ .filter((field) => field.searchable)
93
+ .map((field) => field.key),
94
+ selectColumns: entityDef.data.selectColumns,
95
+ columnMap: entityDef.data.columnMap,
96
+ filters: entityDef.data.filters,
97
+ defaults: entityDef.data.defaults,
98
+ }), cacheOptions)
99
+ }
100
+
101
+ if (entityDef.data?.table) {
102
+ console.warn(
103
+ `[fayz/core] resolveDataProvider: falling back to mock for "${entityDef.name}" (table: ${entityDef.data.table}). ` +
104
+ `Supabase client ${client ? 'available' : 'NOT initialized'}.`,
105
+ )
106
+ }
107
+ return createMockProvider(entityDef, mockData)
108
+ }