@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.
- package/README.md +2 -2
- package/dist/access/index.js.map +1 -1
- package/dist/{chunk-L4TLMYGC.js → chunk-6RBW2Y5K.js} +3 -3
- package/dist/chunk-6RBW2Y5K.js.map +1 -0
- package/dist/chunk-732GZEUI.js +1592 -0
- package/dist/chunk-732GZEUI.js.map +1 -0
- package/dist/{chunk-X5THHG6T.js → chunk-CPMHCTX3.js} +3 -3
- package/dist/chunk-CPMHCTX3.js.map +1 -0
- package/dist/{chunk-V2MSAUHO.js → chunk-JO7Z2AOE.js} +2 -2
- package/dist/chunk-JO7Z2AOE.js.map +1 -0
- package/dist/{chunk-5RJVUC6J.js → chunk-M27JPX3K.js} +12 -3
- package/dist/chunk-M27JPX3K.js.map +1 -0
- package/dist/chunk-OXQGRJZT.js +125 -0
- package/dist/chunk-OXQGRJZT.js.map +1 -0
- package/dist/data/backend-env.d.ts +38 -0
- package/dist/data/backend-env.d.ts.map +1 -0
- package/dist/data/index.js +1 -1
- package/dist/entity/events.d.ts +14 -0
- package/dist/entity/events.d.ts.map +1 -0
- package/dist/events/envelope.d.ts +31 -0
- package/dist/events/envelope.d.ts.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/log.d.ts +44 -0
- package/dist/events/log.d.ts.map +1 -0
- package/dist/events/types.d.ts +40 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +10 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/index.d.ts +27 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +388 -52
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.js +1 -1
- package/dist/{integrations/index.d.ts → integrations.d.ts} +2 -2
- package/dist/integrations.d.ts.map +1 -0
- package/dist/{address/index.d.ts → lib/address.d.ts} +1 -1
- package/dist/{address/index.d.ts.map → lib/address.d.ts.map} +1 -1
- package/dist/{phone/index.d.ts → lib/phone.d.ts} +1 -1
- package/dist/lib/phone.d.ts.map +1 -0
- package/dist/{seo/index.d.ts → lib/seo.d.ts} +18 -1
- package/dist/lib/seo.d.ts.map +1 -0
- package/dist/{tenant/index.d.ts → lib/tenant.d.ts} +1 -1
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/manifest/index.d.ts +1 -1
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/messaging/index.d.ts +3 -0
- package/dist/messaging/index.d.ts.map +1 -0
- package/dist/messaging/registry.d.ts +35 -0
- package/dist/messaging/registry.d.ts.map +1 -0
- package/dist/messaging/types.d.ts +67 -0
- package/dist/messaging/types.d.ts.map +1 -0
- package/dist/plugin/index.js +2 -1
- package/dist/plugin/runtime.d.ts +17 -0
- package/dist/plugin/runtime.d.ts.map +1 -1
- package/dist/registry/block-catalog.d.ts +78 -0
- package/dist/registry/block-catalog.d.ts.map +1 -0
- package/dist/registry/block-constraints.d.ts +64 -0
- package/dist/registry/block-constraints.d.ts.map +1 -0
- package/dist/registry/block-schema.d.ts +69 -0
- package/dist/registry/block-schema.d.ts.map +1 -0
- package/dist/registry/blocks.d.ts +84 -0
- package/dist/registry/blocks.d.ts.map +1 -0
- package/dist/registry/entities.d.ts +153 -0
- package/dist/registry/entities.d.ts.map +1 -0
- package/dist/registry/extension-points.d.ts +117 -0
- package/dist/registry/extension-points.d.ts.map +1 -0
- package/dist/registry/handles.d.ts +41 -0
- package/dist/registry/handles.d.ts.map +1 -0
- package/dist/registry/index.d.ts +41 -41
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/loaders.d.ts +55 -0
- package/dist/registry/loaders.d.ts.map +1 -0
- package/dist/registry/registry-class.d.ts +157 -0
- package/dist/registry/registry-class.d.ts.map +1 -0
- package/dist/registry/schema-check.d.ts +14 -0
- package/dist/registry/schema-check.d.ts.map +1 -0
- package/dist/router/index.d.ts +78 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/{testing/index.d.ts → testing.d.ts} +3 -3
- package/dist/testing.d.ts.map +1 -0
- package/dist/types/analytics.d.ts +196 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/crud.d.ts +22 -2
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +55 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +6 -18
- package/dist/blocks/index.d.ts +0 -25
- package/dist/blocks/index.d.ts.map +0 -1
- package/dist/chunk-5RJVUC6J.js.map +0 -1
- package/dist/chunk-L4TLMYGC.js.map +0 -1
- package/dist/chunk-V2MSAUHO.js.map +0 -1
- package/dist/chunk-X5THHG6T.js.map +0 -1
- package/dist/chunk-Y2C2FBQP.js +0 -35
- package/dist/chunk-Y2C2FBQP.js.map +0 -1
- package/dist/chunk-YH25Y4FW.js +0 -311
- package/dist/chunk-YH25Y4FW.js.map +0 -1
- package/dist/integrations/index.d.ts.map +0 -1
- package/dist/phone/index.d.ts.map +0 -1
- package/dist/seo/index.d.ts.map +0 -1
- package/dist/tenant/index.d.ts.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/src/access/denial.ts +0 -33
- package/src/access/engine.test.ts +0 -48
- package/src/access/engine.ts +0 -136
- package/src/access/index.ts +0 -14
- package/src/access/limits.ts +0 -18
- package/src/address/index.ts +0 -188
- package/src/app/render.tsx +0 -69
- package/src/blocks/index.tsx +0 -68
- package/src/data/archetype.ts +0 -255
- package/src/data/bulk.ts +0 -35
- package/src/data/cached.ts +0 -55
- package/src/data/count.test.ts +0 -152
- package/src/data/count.ts +0 -131
- package/src/data/filters.test.ts +0 -70
- package/src/data/filters.ts +0 -90
- package/src/data/index.ts +0 -11
- package/src/data/mock.ts +0 -90
- package/src/data/platform-api.ts +0 -209
- package/src/data/refresh.ts +0 -115
- package/src/data/resolve.ts +0 -126
- package/src/data/supabase.ts +0 -230
- package/src/data/types.ts +0 -50
- package/src/entity/index.ts +0 -2
- package/src/entity/registry.ts +0 -49
- package/src/events/index.tsx +0 -82
- package/src/i18n/index.ts +0 -476
- package/src/i18n/shell-translations.en.ts +0 -875
- package/src/i18n/shell-translations.pt-br.ts +0 -873
- package/src/index.ts +0 -217
- package/src/integrations/index.ts +0 -237
- package/src/lib/cache.ts +0 -118
- package/src/lib/csv.ts +0 -54
- package/src/lib/format.ts +0 -58
- package/src/manifest/app-manifest.schema.json +0 -676
- package/src/manifest/index.ts +0 -518
- package/src/manifest/serialize.ts +0 -52
- package/src/phone/index.ts +0 -85
- package/src/plugin/createSafeDataProvider.ts +0 -30
- package/src/plugin/index.ts +0 -1
- package/src/plugin/runtime.ts +0 -331
- package/src/registry/index.ts +0 -163
- package/src/router/history.ts +0 -86
- package/src/router/index.ts +0 -29
- package/src/runtime/index.ts +0 -15
- package/src/runtime/oauth.ts +0 -219
- package/src/search/engine.ts +0 -559
- package/src/search/index.ts +0 -25
- package/src/search/text.ts +0 -206
- package/src/search/types.ts +0 -75
- package/src/seo/index.ts +0 -128
- package/src/tenant/index.ts +0 -38
- package/src/testing/index.ts +0 -116
- package/src/types/auth.ts +0 -41
- package/src/types/billing.ts +0 -39
- package/src/types/crud.ts +0 -184
- package/src/types/entities.ts +0 -142
- package/src/types/entitlements.ts +0 -45
- package/src/types/i18n.ts +0 -7
- package/src/types/index.ts +0 -11
- package/src/types/org.ts +0 -137
- package/src/types/payment.ts +0 -44
- package/src/types/permissions.ts +0 -32
- package/src/types/plugins.ts +0 -480
- package/src/types/theme.ts +0 -8
package/src/data/refresh.ts
DELETED
|
@@ -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
|
-
}
|
package/src/data/resolve.ts
DELETED
|
@@ -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
|
-
}
|
package/src/data/supabase.ts
DELETED
|
@@ -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
|
-
}
|
package/src/entity/index.ts
DELETED
package/src/entity/registry.ts
DELETED
|
@@ -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
|
-
}
|
package/src/events/index.tsx
DELETED
|
@@ -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
|
-
}
|