@fayz-ai/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +2 -2
  2. package/dist/access/index.js.map +1 -1
  3. package/dist/{chunk-L4TLMYGC.js → chunk-6RBW2Y5K.js} +3 -3
  4. package/dist/chunk-6RBW2Y5K.js.map +1 -0
  5. package/dist/chunk-732GZEUI.js +1592 -0
  6. package/dist/chunk-732GZEUI.js.map +1 -0
  7. package/dist/{chunk-X5THHG6T.js → chunk-CPMHCTX3.js} +3 -3
  8. package/dist/chunk-CPMHCTX3.js.map +1 -0
  9. package/dist/{chunk-V2MSAUHO.js → chunk-JO7Z2AOE.js} +2 -2
  10. package/dist/chunk-JO7Z2AOE.js.map +1 -0
  11. package/dist/{chunk-5RJVUC6J.js → chunk-M27JPX3K.js} +12 -3
  12. package/dist/chunk-M27JPX3K.js.map +1 -0
  13. package/dist/chunk-OXQGRJZT.js +125 -0
  14. package/dist/chunk-OXQGRJZT.js.map +1 -0
  15. package/dist/data/backend-env.d.ts +38 -0
  16. package/dist/data/backend-env.d.ts.map +1 -0
  17. package/dist/data/index.js +1 -1
  18. package/dist/entity/events.d.ts +14 -0
  19. package/dist/entity/events.d.ts.map +1 -0
  20. package/dist/events/envelope.d.ts +31 -0
  21. package/dist/events/envelope.d.ts.map +1 -0
  22. package/dist/events/index.d.ts +4 -0
  23. package/dist/events/index.d.ts.map +1 -1
  24. package/dist/events/log.d.ts +44 -0
  25. package/dist/events/log.d.ts.map +1 -0
  26. package/dist/events/types.d.ts +40 -0
  27. package/dist/events/types.d.ts.map +1 -0
  28. package/dist/i18n/index.d.ts +10 -0
  29. package/dist/i18n/index.d.ts.map +1 -1
  30. package/dist/i18n/index.js +1 -1
  31. package/dist/index.d.ts +27 -17
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +388 -52
  34. package/dist/index.js.map +1 -1
  35. package/dist/integrations/index.js +1 -1
  36. package/dist/{integrations/index.d.ts → integrations.d.ts} +2 -2
  37. package/dist/integrations.d.ts.map +1 -0
  38. package/dist/{address/index.d.ts → lib/address.d.ts} +1 -1
  39. package/dist/{address/index.d.ts.map → lib/address.d.ts.map} +1 -1
  40. package/dist/{phone/index.d.ts → lib/phone.d.ts} +1 -1
  41. package/dist/lib/phone.d.ts.map +1 -0
  42. package/dist/{seo/index.d.ts → lib/seo.d.ts} +18 -1
  43. package/dist/lib/seo.d.ts.map +1 -0
  44. package/dist/{tenant/index.d.ts → lib/tenant.d.ts} +1 -1
  45. package/dist/lib/tenant.d.ts.map +1 -0
  46. package/dist/manifest/index.d.ts +1 -1
  47. package/dist/manifest/index.d.ts.map +1 -1
  48. package/dist/messaging/index.d.ts +3 -0
  49. package/dist/messaging/index.d.ts.map +1 -0
  50. package/dist/messaging/registry.d.ts +35 -0
  51. package/dist/messaging/registry.d.ts.map +1 -0
  52. package/dist/messaging/types.d.ts +67 -0
  53. package/dist/messaging/types.d.ts.map +1 -0
  54. package/dist/plugin/index.js +2 -1
  55. package/dist/plugin/runtime.d.ts +17 -0
  56. package/dist/plugin/runtime.d.ts.map +1 -1
  57. package/dist/registry/block-catalog.d.ts +78 -0
  58. package/dist/registry/block-catalog.d.ts.map +1 -0
  59. package/dist/registry/block-constraints.d.ts +64 -0
  60. package/dist/registry/block-constraints.d.ts.map +1 -0
  61. package/dist/registry/block-schema.d.ts +69 -0
  62. package/dist/registry/block-schema.d.ts.map +1 -0
  63. package/dist/registry/blocks.d.ts +84 -0
  64. package/dist/registry/blocks.d.ts.map +1 -0
  65. package/dist/registry/entities.d.ts +153 -0
  66. package/dist/registry/entities.d.ts.map +1 -0
  67. package/dist/registry/extension-points.d.ts +117 -0
  68. package/dist/registry/extension-points.d.ts.map +1 -0
  69. package/dist/registry/handles.d.ts +41 -0
  70. package/dist/registry/handles.d.ts.map +1 -0
  71. package/dist/registry/index.d.ts +41 -41
  72. package/dist/registry/index.d.ts.map +1 -1
  73. package/dist/registry/loaders.d.ts +55 -0
  74. package/dist/registry/loaders.d.ts.map +1 -0
  75. package/dist/registry/registry-class.d.ts +157 -0
  76. package/dist/registry/registry-class.d.ts.map +1 -0
  77. package/dist/registry/schema-check.d.ts +14 -0
  78. package/dist/registry/schema-check.d.ts.map +1 -0
  79. package/dist/router/index.d.ts +78 -1
  80. package/dist/router/index.d.ts.map +1 -1
  81. package/dist/router/index.js +1 -1
  82. package/dist/testing/index.js +1 -1
  83. package/dist/{testing/index.d.ts → testing.d.ts} +3 -3
  84. package/dist/testing.d.ts.map +1 -0
  85. package/dist/types/analytics.d.ts +196 -0
  86. package/dist/types/analytics.d.ts.map +1 -0
  87. package/dist/types/crud.d.ts +22 -2
  88. package/dist/types/crud.d.ts.map +1 -1
  89. package/dist/types/plugins.d.ts +55 -1
  90. package/dist/types/plugins.d.ts.map +1 -1
  91. package/package.json +6 -18
  92. package/dist/blocks/index.d.ts +0 -25
  93. package/dist/blocks/index.d.ts.map +0 -1
  94. package/dist/chunk-5RJVUC6J.js.map +0 -1
  95. package/dist/chunk-L4TLMYGC.js.map +0 -1
  96. package/dist/chunk-V2MSAUHO.js.map +0 -1
  97. package/dist/chunk-X5THHG6T.js.map +0 -1
  98. package/dist/chunk-Y2C2FBQP.js +0 -35
  99. package/dist/chunk-Y2C2FBQP.js.map +0 -1
  100. package/dist/chunk-YH25Y4FW.js +0 -311
  101. package/dist/chunk-YH25Y4FW.js.map +0 -1
  102. package/dist/integrations/index.d.ts.map +0 -1
  103. package/dist/phone/index.d.ts.map +0 -1
  104. package/dist/seo/index.d.ts.map +0 -1
  105. package/dist/tenant/index.d.ts.map +0 -1
  106. package/dist/testing/index.d.ts.map +0 -1
  107. package/src/access/denial.ts +0 -33
  108. package/src/access/engine.test.ts +0 -48
  109. package/src/access/engine.ts +0 -136
  110. package/src/access/index.ts +0 -14
  111. package/src/access/limits.ts +0 -18
  112. package/src/address/index.ts +0 -188
  113. package/src/app/render.tsx +0 -69
  114. package/src/blocks/index.tsx +0 -68
  115. package/src/data/archetype.ts +0 -255
  116. package/src/data/bulk.ts +0 -35
  117. package/src/data/cached.ts +0 -55
  118. package/src/data/count.test.ts +0 -152
  119. package/src/data/count.ts +0 -131
  120. package/src/data/filters.test.ts +0 -70
  121. package/src/data/filters.ts +0 -90
  122. package/src/data/index.ts +0 -11
  123. package/src/data/mock.ts +0 -90
  124. package/src/data/platform-api.ts +0 -209
  125. package/src/data/refresh.ts +0 -115
  126. package/src/data/resolve.ts +0 -126
  127. package/src/data/supabase.ts +0 -230
  128. package/src/data/types.ts +0 -50
  129. package/src/entity/index.ts +0 -2
  130. package/src/entity/registry.ts +0 -49
  131. package/src/events/index.tsx +0 -82
  132. package/src/i18n/index.ts +0 -476
  133. package/src/i18n/shell-translations.en.ts +0 -875
  134. package/src/i18n/shell-translations.pt-br.ts +0 -873
  135. package/src/index.ts +0 -217
  136. package/src/integrations/index.ts +0 -237
  137. package/src/lib/cache.ts +0 -118
  138. package/src/lib/csv.ts +0 -54
  139. package/src/lib/format.ts +0 -58
  140. package/src/manifest/app-manifest.schema.json +0 -676
  141. package/src/manifest/index.ts +0 -518
  142. package/src/manifest/serialize.ts +0 -52
  143. package/src/phone/index.ts +0 -85
  144. package/src/plugin/createSafeDataProvider.ts +0 -30
  145. package/src/plugin/index.ts +0 -1
  146. package/src/plugin/runtime.ts +0 -331
  147. package/src/registry/index.ts +0 -163
  148. package/src/router/history.ts +0 -86
  149. package/src/router/index.ts +0 -29
  150. package/src/runtime/index.ts +0 -15
  151. package/src/runtime/oauth.ts +0 -219
  152. package/src/search/engine.ts +0 -559
  153. package/src/search/index.ts +0 -25
  154. package/src/search/text.ts +0 -206
  155. package/src/search/types.ts +0 -75
  156. package/src/seo/index.ts +0 -128
  157. package/src/tenant/index.ts +0 -38
  158. package/src/testing/index.ts +0 -116
  159. package/src/types/auth.ts +0 -41
  160. package/src/types/billing.ts +0 -39
  161. package/src/types/crud.ts +0 -184
  162. package/src/types/entities.ts +0 -142
  163. package/src/types/entitlements.ts +0 -45
  164. package/src/types/i18n.ts +0 -7
  165. package/src/types/index.ts +0 -11
  166. package/src/types/org.ts +0 -137
  167. package/src/types/payment.ts +0 -44
  168. package/src/types/permissions.ts +0 -32
  169. package/src/types/plugins.ts +0 -480
  170. package/src/types/theme.ts +0 -8
@@ -1,115 +0,0 @@
1
- import * as React from 'react'
2
- import { eventBus } from '../events'
3
- import { globalCache } from '../lib/cache'
4
- import { getActiveTenantId } from '../tenant'
5
-
6
- // ---------------------------------------------------------------------------
7
- // Data refresh bus — "something wrote; whoever is showing it should reload".
8
- //
9
- // A write that happens OUTSIDE the list showing it (the assistant creating an
10
- // appointment, a webhook, another tab) used to leave the screen stale until the
11
- // user reloaded by hand. Providers already invalidate their own read cache on
12
- // create/update/remove, but a mounted list holds its rows in a store — nothing
13
- // told it to fetch again.
14
- //
15
- // This is the missing half: one broadcast, every surface that renders the
16
- // affected data listens. Deliberately coarse — a table name, not a row diff.
17
- // Refetching a page of rows is cheap; a stale screen after "criei o
18
- // agendamento" is not.
19
- // ---------------------------------------------------------------------------
20
-
21
- export const DATA_CHANGED_EVENT = 'data:changed'
22
-
23
- export interface DataChangedPayload {
24
- /** Physical table the write touched, when the writer knows it. */
25
- table?: string
26
- /** Registered entity key (deriveEntityKey), when the writer knows it. */
27
- entityKey?: string
28
- /** Archetype the record belongs to (`person:client`), when known. */
29
- archetype?: string
30
- op?: 'create' | 'update' | 'delete' | 'unknown'
31
- /** Affected record id, when known. */
32
- id?: string
33
- /** What caused the write — lets a surface ignore its own UI writes. */
34
- source?: 'agent' | 'ui' | 'remote'
35
- }
36
-
37
- /** What a subscriber renders. Any field left out matches everything. */
38
- export interface DataChangedMatch {
39
- table?: string
40
- entityKey?: string
41
- archetype?: string
42
- }
43
-
44
- /**
45
- * A write with NO table/entityKey is a write we could not attribute (a plugin
46
- * tool, an RPC). Subscribers still refresh for it: a redundant refetch costs a
47
- * request, a missed one costs the user's trust in what the screen shows.
48
- */
49
- export function matchesDataChange(match: DataChangedMatch, payload: DataChangedPayload): boolean {
50
- const attributed = payload.table ?? payload.entityKey ?? payload.archetype
51
- if (!attributed) return true
52
- if (match.table && payload.table && match.table === payload.table) return true
53
- if (match.entityKey && payload.entityKey && match.entityKey === payload.entityKey) return true
54
- if (match.archetype && payload.archetype && match.archetype === payload.archetype) return true
55
- // A subscriber that declared nothing listens to everything.
56
- return !match.table && !match.entityKey && !match.archetype
57
- }
58
-
59
- // One agent turn can write twice (create + link). Coalesce so the lists behind
60
- // it fetch once, after the turn settles, instead of once per call.
61
- let pending: DataChangedPayload[] = []
62
- let flushTimer: ReturnType<typeof setTimeout> | null = null
63
-
64
- function flush(): void {
65
- const batch = pending
66
- pending = []
67
- flushTimer = null
68
- for (const payload of batch) eventBus.emit(DATA_CHANGED_EVENT, payload)
69
- }
70
-
71
- /**
72
- * Announce a write. Invalidates the provider read cache for the affected table
73
- * (an unattributed write clears it wholesale — a stale hit would defeat the
74
- * refetch it just triggered) and broadcasts to mounted surfaces.
75
- */
76
- export function emitDataChanged(payload: DataChangedPayload = {}): void {
77
- if (payload.table) {
78
- const tenant = getActiveTenantId() ?? '_'
79
- // The base table and its read-model view are the SAME data under two cache
80
- // namespaces (appointments / v_appointments). Invalidating only the one the
81
- // writer names leaves the other serving a snapshot that predates the write
82
- // — which is how the assistant kept reporting an empty agenda after a
83
- // booking was created on the agenda screen.
84
- const sibling = payload.table.startsWith('v_')
85
- ? payload.table.slice(2)
86
- : `v_${payload.table}`
87
- globalCache.invalidate(`${tenant}:${payload.table}`)
88
- globalCache.invalidate(`${tenant}:${sibling}`)
89
- } else {
90
- globalCache.clear()
91
- }
92
- pending.push({ op: 'unknown', source: 'agent', ...payload })
93
- if (flushTimer) return
94
- flushTimer = setTimeout(flush, 120)
95
- }
96
-
97
- /** Subscribe for the lifetime of the component. The latest handler always runs. */
98
- export function useDataChanged(
99
- match: DataChangedMatch,
100
- handler: (payload: DataChangedPayload) => void,
101
- deps: React.DependencyList = [],
102
- ): void {
103
- const handlerRef = React.useRef(handler)
104
- handlerRef.current = handler
105
- const { table, entityKey, archetype } = match
106
-
107
- React.useEffect(
108
- () =>
109
- eventBus.on<DataChangedPayload>(DATA_CHANGED_EVENT, (payload) => {
110
- if (matchesDataChange({ table, entityKey, archetype }, payload)) handlerRef.current(payload)
111
- }),
112
- // eslint-disable-next-line react-hooks/exhaustive-deps
113
- [table, entityKey, archetype, ...deps],
114
- )
115
- }
@@ -1,126 +0,0 @@
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 './platform-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
- /**
23
- * Read through the shared TTL cache (default true).
24
- *
25
- * Callers that must observe writes made by ANOTHER surface pass false: the
26
- * cache is only invalidated by writes that go through it, so a record created
27
- * on the agenda screen stays invisible to the assistant for the rest of the
28
- * TTL — which is how "o que tem na agenda hoje?" answered "nada" over a
29
- * booking made seconds earlier.
30
- */
31
- cache?: boolean
32
- customProviders?: Record<string, <T extends { id: string }>(entityDef: EntityDef<T>, mockData?: T[]) => DataProvider<T>>
33
- }
34
-
35
- export function resolveDataProvider<T extends { id: string }>(
36
- entityDef: EntityDef<T>,
37
- mockData?: T[],
38
- options: ResolveDataProviderOptions = {},
39
- ): DataProvider<T> {
40
- const backend = options.backend
41
-
42
- if (backend?.provider === 'mock') {
43
- return createMockProvider(entityDef, mockData)
44
- }
45
-
46
- if (backend?.provider === 'fayz-api') {
47
- const table = entityDef.data?.table ?? entityDef.name
48
- return createFayzApiProvider<T>(table, {
49
- ...options.fayzApi,
50
- baseUrl: options.fayzApi?.baseUrl ?? backend.url,
51
- projectId: options.fayzApi?.projectId ?? backend.projectRef,
52
- entityKey: entityDef.name ?? table,
53
- table,
54
- schema: options.fayzApi?.schema ?? entityDef.data?.schema,
55
- idColumn: options.fayzApi?.idColumn ?? entityDef.data?.columnMap?.id,
56
- searchColumns: options.fayzApi?.searchColumns ?? entityDef.data?.searchColumns ?? entityDef.fields
57
- .filter((field) => field.searchable)
58
- .map((field) => field.key),
59
- tenantIdColumn: entityDef.data?.tenantScoped === false
60
- ? false
61
- : options.fayzApi?.tenantIdColumn ?? entityDef.data?.tenantIdColumn,
62
- tenantId: options.fayzApi?.tenantId ?? (() => getActiveTenantId()),
63
- })
64
- }
65
-
66
- if (backend?.provider === 'custom') {
67
- const adapterId = backend.adapterId
68
- const factory = adapterId ? options.customProviders?.[adapterId] : undefined
69
- if (!adapterId || !factory) {
70
- throw new Error(`[@fayz-ai/core] Custom data provider "${adapterId ?? 'unknown'}" is not registered.`)
71
- }
72
- return factory<T>(entityDef, mockData)
73
- }
74
-
75
- const client = getSupabaseClientOptional()
76
-
77
- if (client && entityDef.data?.table) {
78
- const tenantId = () => getActiveTenantId()
79
- // Archetype entities share a physical table (e.g. people) across kinds
80
- // (student/teacher/staff…). The cache key must carry the kind or the first
81
- // kind's resultset is served to every sibling entity (teachers page
82
- // listing students).
83
- const cacheOptions = {
84
- table: entityDef.data.archetypeKind
85
- ? `${entityDef.data.table}#${entityDef.data.archetypeKind}`
86
- : entityDef.data.table,
87
- tenantId,
88
- ttl: entityDef.data.cacheTTL,
89
- }
90
- const cached = <P extends DataProvider<T>>(p: P): DataProvider<T> =>
91
- options.cache === false ? p : withCache(p, cacheOptions)
92
-
93
- if (entityDef.data.archetype && entityDef.data.archetypeKind && !entityDef.data.schema) {
94
- return cached(createArchetypeProvider<T>({
95
- archetype: entityDef.data.archetype,
96
- archetypeKind: entityDef.data.archetypeKind,
97
- projectTable: entityDef.data.table,
98
- tenantId,
99
- searchColumns: entityDef.data.searchColumns ?? entityDef.fields
100
- .filter((field) => field.searchable)
101
- .map((field) => field.key),
102
- }))
103
- }
104
-
105
- return cached(createSupabaseProvider<T>(entityDef.data.table, {
106
- schema: entityDef.data.schema,
107
- tenantId: entityDef.data.tenantScoped === false ? undefined : tenantId,
108
- tenantIdColumn: entityDef.data.tenantIdColumn,
109
- searchColumns: entityDef.data.searchColumns ?? entityDef.fields
110
- .filter((field) => field.searchable)
111
- .map((field) => field.key),
112
- selectColumns: entityDef.data.selectColumns,
113
- columnMap: entityDef.data.columnMap,
114
- filters: entityDef.data.filters,
115
- defaults: entityDef.data.defaults,
116
- }))
117
- }
118
-
119
- if (entityDef.data?.table) {
120
- console.warn(
121
- `[fayz/core] resolveDataProvider: falling back to mock for "${entityDef.name}" (table: ${entityDef.data.table}). ` +
122
- `Supabase client ${client ? 'available' : 'NOT initialized'}.`,
123
- )
124
- }
125
- return createMockProvider(entityDef, mockData)
126
- }
@@ -1,230 +0,0 @@
1
- import type { DataProvider, CrudQuery, CrudResult } from './types'
2
- import { applyRangeToQuery, isFilterObject, parseRangeFilter } from './filters'
3
-
4
- export interface SupabaseProviderConfig {
5
- schema?: string
6
- tenantId?: string | (() => string | undefined)
7
- tenantIdColumn?: string
8
- searchColumns?: string[]
9
- selectColumns?: string
10
- columnMap?: Record<string, string>
11
- filters?: Record<string, string>
12
- defaults?: Record<string, unknown>
13
- supabaseClient?: unknown
14
- }
15
-
16
- function camelToSnake(str: string): string {
17
- return str.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase())
18
- }
19
-
20
- function snakeToCamel(str: string): string {
21
- return str.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
22
- }
23
-
24
- function mapRow<T>(row: Record<string, unknown>, columnMap?: Record<string, string>): T {
25
- const result: Record<string, unknown> = {}
26
- for (const [key, value] of Object.entries(row)) {
27
- const camelKey = columnMap?.[key] ?? snakeToCamel(key)
28
- result[camelKey] = value
29
- }
30
- return result as T
31
- }
32
-
33
- function mapEntity(entity: Record<string, unknown>, columnMap?: Record<string, string>): Record<string, unknown> {
34
- const reverseMap: Record<string, string> = {}
35
- if (columnMap) {
36
- for (const [snake, camel] of Object.entries(columnMap)) {
37
- reverseMap[camel] = snake
38
- }
39
- }
40
- const result: Record<string, unknown> = {}
41
- for (const [key, value] of Object.entries(entity)) {
42
- if (key === 'id' || key === 'createdAt' || key === 'updatedAt' || key === 'tenantId') continue
43
- const snakeKey = reverseMap[key] ?? camelToSnake(key)
44
- result[snakeKey] = value
45
- }
46
- return result
47
- }
48
-
49
- // Global Supabase client registry — set once via createFayzApp
50
- let _globalSupabaseClient: unknown | null = null
51
-
52
- export function setGlobalSupabaseClient(client: unknown): void {
53
- _globalSupabaseClient = client
54
- }
55
-
56
- export function getSupabaseClientOptional(): unknown | null {
57
- return _globalSupabaseClient
58
- }
59
-
60
- export function createSupabaseProvider<T extends { id: string }>(
61
- table: string,
62
- config?: SupabaseProviderConfig,
63
- ): DataProvider<T> {
64
- const schema = config?.schema ?? 'public'
65
- const tenantIdCol = config?.tenantIdColumn ?? 'tenant_id'
66
- const searchCols = config?.searchColumns ?? []
67
- const selectCols = config?.selectColumns ?? '*'
68
- const columnMap = config?.columnMap
69
-
70
- function resolveTenantId(): string | undefined {
71
- if (!config?.tenantId) return undefined
72
- return typeof config.tenantId === 'function' ? config.tenantId() : config.tenantId
73
- }
74
-
75
- function getClient() {
76
- const supabase = (config?.supabaseClient ?? _globalSupabaseClient) as {
77
- schema: (s: string) => unknown
78
- from: (t: string) => unknown
79
- } | null
80
- if (!supabase) throw new Error(`[@fayz-ai/core] Supabase client not available. Call createFayzApp with a provider first.`)
81
- return schema === 'public' ? (supabase as { from: (t: string) => unknown }) : (supabase.schema(schema) as { from: (t: string) => unknown })
82
- }
83
-
84
- return {
85
- async list(query: CrudQuery): Promise<CrudResult<T>> {
86
- // 'none' skips PostgREST's count aggregate entirely — see CrudQuery.countMode.
87
- const wantCount = query.countMode !== 'none'
88
- let q = (getClient().from(table) as { select: (...args: unknown[]) => unknown })
89
- .select(selectCols, wantCount ? { count: 'exact' } : undefined) as Record<string, unknown>
90
-
91
- const tenantId = resolveTenantId()
92
- if (tenantId) q = (q as { eq: (col: string, val: string) => unknown }).eq(tenantIdCol, tenantId) as Record<string, unknown>
93
-
94
- if (config?.filters) {
95
- for (const [col, val] of Object.entries(config.filters)) {
96
- q = (q as { eq: (c: string, v: string) => unknown }).eq(col, val) as Record<string, unknown>
97
- }
98
- }
99
-
100
- // Faceted / dynamic filters from the query, mapped to columns. Equality
101
- // by default; a range object ({from,to} / {gte,lt} / {$gte,$lt}) becomes
102
- // real SQL bounds instead of an equality compare against "[object Object]".
103
- if (query.filters) {
104
- for (const [key, val] of Object.entries(query.filters)) {
105
- if (val == null || val === '') continue
106
- const col = columnMap?.[key] ?? camelToSnake(key)
107
- const range = parseRangeFilter(val)
108
- if (range) {
109
- q = applyRangeToQuery(q, col, range)
110
- continue
111
- }
112
- if (isFilterObject(val)) {
113
- throw new Error(
114
- `[@fayz-ai/core] Filter "${key}" is an object with no recognised bound. ` +
115
- 'Use {from,to} / {gte,lte,gt,lt} for ranges, or a plain value for equality.',
116
- )
117
- }
118
- q = (q as { eq: (c: string, v: unknown) => unknown }).eq(col, val) as Record<string, unknown>
119
- }
120
- }
121
-
122
- if (query.search && searchCols.length > 0) {
123
- const term = `%${query.search}%`
124
- const orClause = searchCols.map((col) => `${camelToSnake(col)}.ilike.${term}`).join(',')
125
- q = (q as { or: (c: string) => unknown }).or(orClause) as Record<string, unknown>
126
- }
127
-
128
- if (query.sortBy) {
129
- q = (q as { order: (col: string, opts: unknown) => unknown })
130
- .order(camelToSnake(query.sortBy), { ascending: query.sortDir !== 'desc' }) as Record<string, unknown>
131
- } else {
132
- q = (q as { order: (col: string, opts: unknown) => unknown })
133
- .order('created_at', { ascending: false }) as Record<string, unknown>
134
- }
135
-
136
- const page = query.page ?? 1
137
- const pageSize = query.pageSize ?? 50
138
- const from = (page - 1) * pageSize
139
- q = (q as { range: (f: number, t: number) => unknown }).range(from, from + pageSize - 1) as Record<string, unknown>
140
-
141
- const { data, error, count } = await q as { data: Record<string, unknown>[] | null; error: unknown; count: number | null }
142
- if (error) throw error
143
-
144
- return {
145
- data: (data ?? []).map((row) => mapRow<T>(row, columnMap)),
146
- total: count ?? 0,
147
- }
148
- },
149
-
150
- async create(data) {
151
- const row = mapEntity(data as Record<string, unknown>, columnMap)
152
- const tenantId = resolveTenantId()
153
- if (tenantId) row[tenantIdCol] = tenantId
154
- if (config?.defaults) {
155
- for (const [col, val] of Object.entries(config.defaults)) {
156
- if (row[col] === undefined) row[col] = val
157
- }
158
- }
159
-
160
- const { data: created, error } = (await (getClient().from(table) as {
161
- insert: (r: unknown) => { select: () => { single: () => Promise<{ data: unknown; error: unknown }> } }
162
- }).insert(row).select().single()) as { data: Record<string, unknown> | null; error: unknown }
163
-
164
- if (error) throw error
165
- return mapRow<T>(created!, columnMap)
166
- },
167
-
168
- async createMany(rows, options) {
169
- const tenantId = resolveTenantId()
170
- const size = Math.max(1, options?.chunkSize ?? 200)
171
- const prepared = rows.map((r) => {
172
- const row = mapEntity(r as Record<string, unknown>, columnMap)
173
- if (tenantId) row[tenantIdCol] = tenantId
174
- if (config?.defaults) {
175
- for (const [col, val] of Object.entries(config.defaults)) {
176
- if (row[col] === undefined) row[col] = val
177
- }
178
- }
179
- return row
180
- })
181
-
182
- let created = 0
183
- const failed: Array<{ index: number; message: string }> = []
184
- for (let start = 0; start < prepared.length; start += size) {
185
- const chunk = prepared.slice(start, start + size)
186
- const { error } = (await (getClient().from(table) as {
187
- insert: (r: unknown) => Promise<{ error: unknown }>
188
- }).insert(chunk)) as { error: unknown }
189
- if (!error) {
190
- created += chunk.length
191
- } else {
192
- // One bad row rejects its whole chunk. Retrying it row by row keeps
193
- // the other 199 — an import that loses 200 records over one malformed
194
- // phone number is worse than no import.
195
- const message = error instanceof Error ? error.message : String(error)
196
- for (let i = 0; i < chunk.length; i++) {
197
- const { error: rowError } = (await (getClient().from(table) as {
198
- insert: (r: unknown) => Promise<{ error: unknown }>
199
- }).insert(chunk[i])) as { error: unknown }
200
- if (rowError) {
201
- failed.push({
202
- index: start + i,
203
- message: rowError instanceof Error ? rowError.message : String(rowError ?? message),
204
- })
205
- } else created += 1
206
- }
207
- }
208
- options?.onProgress?.(Math.min(start + chunk.length, prepared.length), prepared.length)
209
- }
210
- return { created, failed }
211
- },
212
-
213
- async update(id, data) {
214
- const row = mapEntity(data as Record<string, unknown>, columnMap)
215
- const { data: updated, error } = (await (getClient().from(table) as {
216
- update: (r: unknown) => { eq: (c: string, v: string) => { select: () => { single: () => Promise<{ data: unknown; error: unknown }> } } }
217
- }).update(row).eq('id', id).select().single()) as { data: Record<string, unknown> | null; error: unknown }
218
-
219
- if (error) throw error
220
- return mapRow<T>(updated!, columnMap)
221
- },
222
-
223
- async remove(id) {
224
- const { error } = (await (getClient().from(table) as {
225
- delete: () => { eq: (c: string, v: string) => Promise<{ error: unknown }> }
226
- }).delete().eq('id', id)) as { error: unknown }
227
- if (error) throw error
228
- },
229
- }
230
- }
package/src/data/types.ts DELETED
@@ -1,50 +0,0 @@
1
- export interface CrudQuery {
2
- search?: string
3
- sortBy?: string
4
- sortDir?: 'asc' | 'desc'
5
- page?: number
6
- pageSize?: number
7
- filters?: Record<string, unknown>
8
- /**
9
- * Default 'exact' — a CRUD pager needs the total. It is a second aggregate
10
- * over the filtered set, and on the live pool cost ~1070ms against ~270ms for
11
- * the rows themselves. Callers that only want rows pass 'none' (total: 0).
12
- */
13
- countMode?: 'exact' | 'none'
14
- }
15
-
16
- export interface CrudResult<T> {
17
- data: T[]
18
- total: number
19
- }
20
-
21
- /** Outcome of a bulk write: what landed, and what did not and why. */
22
- export interface BulkWriteResult {
23
- created: number
24
- failed: Array<{ index: number; message: string }>
25
- }
26
-
27
- export interface BulkWriteOptions {
28
- /** Rows per round-trip. Larger is fewer trips; too large trips statement timeouts. */
29
- chunkSize?: number
30
- onProgress?: (done: number, total: number) => void
31
- }
32
-
33
- export interface DataProvider<T extends { id: string } = { id: string }> {
34
- list(query: CrudQuery): Promise<CrudResult<T>>
35
- create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'>): Promise<T>
36
- /**
37
- * Write many rows in as few round-trips as the backend allows.
38
- *
39
- * Optional: `bulkCreate` falls back to looping `create` for providers that do
40
- * not implement it, so a caller never has to branch. Importing a 900-row
41
- * client list one INSERT at a time is 900 round-trips — the difference
42
- * between a progress bar and a hung tab.
43
- */
44
- createMany?(
45
- rows: Array<Omit<T, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'>>,
46
- options?: BulkWriteOptions,
47
- ): Promise<BulkWriteResult>
48
- update(id: string, data: Partial<T>): Promise<T>
49
- remove(id: string): Promise<void>
50
- }
@@ -1,2 +0,0 @@
1
- export { registerEntity, getEntityByKey, getAllEntities, clearEntityRegistry, deriveEntityKey } from './registry'
2
- export type { RegisteredEntity } from './registry'
@@ -1,49 +0,0 @@
1
- import type { EntityDef } from '../types/crud'
2
- import type { EntityArchetype } from '../types/entities'
3
-
4
- export interface RegisteredEntity {
5
- entityKey: string
6
- label: string
7
- labelPlural: string
8
- icon?: string
9
- fields: EntityDef['fields']
10
- source: 'app' | 'plugin'
11
- /**
12
- * The definition itself, so consumers that need to *read* the entity — the AI
13
- * tool executor resolving a data provider — are not limited to the display
14
- * metadata. Optional: registrations made before this existed omit it.
15
- */
16
- entityDef?: EntityDef
17
- mockData?: Array<{ id: string }>
18
- archetype?: EntityArchetype
19
- pluginId?: string
20
- pluginName?: string
21
- pluginIcon?: string
22
- }
23
-
24
- const entityRegistry = new Map<string, RegisteredEntity>()
25
-
26
- export function registerEntity(entry: RegisteredEntity): void {
27
- entityRegistry.set(entry.entityKey, entry)
28
- }
29
-
30
- export function getEntityByKey(key: string): RegisteredEntity | undefined {
31
- return entityRegistry.get(key)
32
- }
33
-
34
- export function getAllEntities(): RegisteredEntity[] {
35
- return Array.from(entityRegistry.values())
36
- }
37
-
38
- export function clearEntityRegistry(): void {
39
- entityRegistry.clear()
40
- }
41
-
42
- export function deriveEntityKey(entityDef: EntityDef): string {
43
- const name = entityDef.name.toLowerCase().replace(/\s+/g, '-')
44
- if (entityDef.data?.archetype && entityDef.data?.archetypeKind) {
45
- return `${entityDef.data.archetype}:${entityDef.data.archetypeKind}`
46
- }
47
- if (entityDef.data?.archetype) return entityDef.data.archetype
48
- return name
49
- }
@@ -1,82 +0,0 @@
1
- import * as React from 'react'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Event bus — the substrate for cross-plugin behavior. Replaces the bespoke
5
- // `window.dispatchEvent('agenda:open-booking')` hack and the imperative
6
- // financial bridge: plugins emit named events, others subscribe, and the
7
- // AppManifest can declare event→action bindings as data. Plugins describe the
8
- // events they emit via PluginManifest.events (see types/plugins) so the
9
- // platform can introspect and wire automations.
10
- // ---------------------------------------------------------------------------
11
-
12
- export type EventHandler<T = unknown> = (payload: T) => void
13
-
14
- export interface EventBus {
15
- emit<T = unknown>(event: string, payload?: T): void
16
- on<T = unknown>(event: string, handler: EventHandler<T>): () => void
17
- once<T = unknown>(event: string, handler: EventHandler<T>): () => void
18
- off<T = unknown>(event: string, handler: EventHandler<T>): void
19
- clear(event?: string): void
20
- }
21
-
22
- export function createEventBus(): EventBus {
23
- const handlers = new Map<string, Set<EventHandler>>()
24
- const bus: EventBus = {
25
- emit(event, payload) {
26
- const set = handlers.get(event)
27
- if (!set) return
28
- // Snapshot so handlers can subscribe/unsubscribe during dispatch.
29
- for (const handler of Array.from(set)) {
30
- try {
31
- handler(payload)
32
- } catch (err) {
33
- // A misbehaving subscriber must not break the emitter or its peers.
34
- console.error(`[fayz] event handler for "${event}" threw:`, err)
35
- }
36
- }
37
- },
38
- on(event, handler) {
39
- let set = handlers.get(event)
40
- if (!set) {
41
- set = new Set()
42
- handlers.set(event, set)
43
- }
44
- set.add(handler as EventHandler)
45
- return () => bus.off(event, handler)
46
- },
47
- once(event, handler) {
48
- const wrapped: EventHandler = (payload) => {
49
- bus.off(event, wrapped)
50
- ;(handler as EventHandler)(payload)
51
- }
52
- return bus.on(event, wrapped)
53
- },
54
- off(event, handler) {
55
- handlers.get(event)?.delete(handler as EventHandler)
56
- },
57
- clear(event) {
58
- if (event) handlers.delete(event)
59
- else handlers.clear()
60
- },
61
- }
62
- return bus
63
- }
64
-
65
- /** The app-wide default bus. One per app runtime (repo-per-app). */
66
- export const eventBus: EventBus = createEventBus()
67
-
68
- /** Subscribe to an event for the lifetime of the component. The latest handler
69
- * is always invoked, so closures over fresh props/state work without resubscribing. */
70
- export function useOnEvent<T = unknown>(
71
- event: string,
72
- handler: EventHandler<T>,
73
- deps: React.DependencyList = [],
74
- ): void {
75
- const ref = React.useRef(handler)
76
- ref.current = handler
77
- React.useEffect(
78
- () => eventBus.on<T>(event, (payload) => ref.current(payload)),
79
- // eslint-disable-next-line react-hooks/exhaustive-deps
80
- [event, ...deps],
81
- )
82
- }