@fayz-ai/core 0.9.0 → 0.11.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 (218) hide show
  1. package/README.md +2 -2
  2. package/dist/access/index.js.map +1 -1
  3. package/dist/chunk-BEGXYHS4.js +566 -0
  4. package/dist/chunk-BEGXYHS4.js.map +1 -0
  5. package/dist/chunk-ED6NBH6U.js +348 -0
  6. package/dist/chunk-ED6NBH6U.js.map +1 -0
  7. package/dist/chunk-EWKX4VUM.js +429 -0
  8. package/dist/chunk-EWKX4VUM.js.map +1 -0
  9. package/dist/chunk-LARZWDHA.js +319 -0
  10. package/dist/chunk-LARZWDHA.js.map +1 -0
  11. package/dist/chunk-OXQGRJZT.js +125 -0
  12. package/dist/chunk-OXQGRJZT.js.map +1 -0
  13. package/dist/chunk-RRYFXUQX.js +1247 -0
  14. package/dist/chunk-RRYFXUQX.js.map +1 -0
  15. package/dist/{chunk-5RJVUC6J.js → chunk-TDI6F6PT.js} +373 -25
  16. package/dist/chunk-TDI6F6PT.js.map +1 -0
  17. package/dist/{chunk-V2MSAUHO.js → chunk-UVKH6RDX.js} +211 -335
  18. package/dist/chunk-UVKH6RDX.js.map +1 -0
  19. package/dist/chunk-V53GR4UT.js +289 -0
  20. package/dist/chunk-V53GR4UT.js.map +1 -0
  21. package/dist/connector-runtime/index.d.ts +15 -0
  22. package/dist/connector-runtime/index.d.ts.map +1 -0
  23. package/dist/connector-runtime/index.js +3 -0
  24. package/dist/connector-runtime/index.js.map +1 -0
  25. package/dist/data/archetype.d.ts.map +1 -1
  26. package/dist/data/backend-env.d.ts +38 -0
  27. package/dist/data/backend-env.d.ts.map +1 -0
  28. package/dist/data/bulk.d.ts +24 -0
  29. package/dist/data/bulk.d.ts.map +1 -1
  30. package/dist/data/cached.d.ts.map +1 -1
  31. package/dist/data/custom-fields.d.ts +5 -0
  32. package/dist/data/custom-fields.d.ts.map +1 -0
  33. package/dist/data/index.d.ts +2 -1
  34. package/dist/data/index.d.ts.map +1 -1
  35. package/dist/data/index.js +3 -1
  36. package/dist/data/mock.d.ts.map +1 -1
  37. package/dist/data/platform-api.d.ts.map +1 -1
  38. package/dist/data/resolve.d.ts.map +1 -1
  39. package/dist/data/supabase.d.ts.map +1 -1
  40. package/dist/data/types.d.ts +8 -0
  41. package/dist/data/types.d.ts.map +1 -1
  42. package/dist/entity/events.d.ts +14 -0
  43. package/dist/entity/events.d.ts.map +1 -0
  44. package/dist/events/envelope.d.ts +31 -0
  45. package/dist/events/envelope.d.ts.map +1 -0
  46. package/dist/events/index.d.ts +4 -0
  47. package/dist/events/index.d.ts.map +1 -1
  48. package/dist/events/log.d.ts +44 -0
  49. package/dist/events/log.d.ts.map +1 -0
  50. package/dist/events/types.d.ts +40 -0
  51. package/dist/events/types.d.ts.map +1 -0
  52. package/dist/fields/store.d.ts +45 -0
  53. package/dist/fields/store.d.ts.map +1 -0
  54. package/dist/i18n/index.d.ts +10 -0
  55. package/dist/i18n/index.d.ts.map +1 -1
  56. package/dist/i18n/index.js +1 -1
  57. package/dist/i18n/shell-translations.en.d.ts.map +1 -1
  58. package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
  59. package/dist/index.d.ts +45 -21
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +543 -56
  62. package/dist/index.js.map +1 -1
  63. package/dist/integrations/events.d.ts +7 -0
  64. package/dist/integrations/events.d.ts.map +1 -0
  65. package/dist/integrations/index.d.ts +533 -29
  66. package/dist/integrations/index.d.ts.map +1 -1
  67. package/dist/integrations/index.js +3 -1
  68. package/dist/integrations/permissions.d.ts +132 -0
  69. package/dist/integrations/permissions.d.ts.map +1 -0
  70. package/dist/integrations/request.d.ts +170 -0
  71. package/dist/integrations/request.d.ts.map +1 -0
  72. package/dist/integrations/runtime-token.d.ts +33 -0
  73. package/dist/integrations/runtime-token.d.ts.map +1 -0
  74. package/dist/integrations/runtime.d.ts +398 -0
  75. package/dist/integrations/runtime.d.ts.map +1 -0
  76. package/dist/integrations/settings.d.ts +102 -0
  77. package/dist/integrations/settings.d.ts.map +1 -0
  78. package/dist/integrations/store.d.ts +145 -0
  79. package/dist/integrations/store.d.ts.map +1 -0
  80. package/dist/{address/index.d.ts → lib/address.d.ts} +1 -1
  81. package/dist/{address/index.d.ts.map → lib/address.d.ts.map} +1 -1
  82. package/dist/{phone/index.d.ts → lib/phone.d.ts} +1 -1
  83. package/dist/lib/phone.d.ts.map +1 -0
  84. package/dist/{seo/index.d.ts → lib/seo.d.ts} +18 -1
  85. package/dist/lib/seo.d.ts.map +1 -0
  86. package/dist/{tenant/index.d.ts → lib/tenant.d.ts} +1 -1
  87. package/dist/lib/tenant.d.ts.map +1 -0
  88. package/dist/manifest/index.d.ts +1 -1
  89. package/dist/manifest/index.d.ts.map +1 -1
  90. package/dist/messaging/index.d.ts +3 -0
  91. package/dist/messaging/index.d.ts.map +1 -0
  92. package/dist/messaging/registry.d.ts +35 -0
  93. package/dist/messaging/registry.d.ts.map +1 -0
  94. package/dist/messaging/types.d.ts +67 -0
  95. package/dist/messaging/types.d.ts.map +1 -0
  96. package/dist/plugin/index.js +3 -1
  97. package/dist/plugin/runtime.d.ts +17 -0
  98. package/dist/plugin/runtime.d.ts.map +1 -1
  99. package/dist/registry/block-catalog.d.ts +78 -0
  100. package/dist/registry/block-catalog.d.ts.map +1 -0
  101. package/dist/registry/block-constraints.d.ts +64 -0
  102. package/dist/registry/block-constraints.d.ts.map +1 -0
  103. package/dist/registry/block-schema.d.ts +69 -0
  104. package/dist/registry/block-schema.d.ts.map +1 -0
  105. package/dist/registry/blocks.d.ts +84 -0
  106. package/dist/registry/blocks.d.ts.map +1 -0
  107. package/dist/registry/entities.d.ts +179 -0
  108. package/dist/registry/entities.d.ts.map +1 -0
  109. package/dist/registry/extension-points.d.ts +117 -0
  110. package/dist/registry/extension-points.d.ts.map +1 -0
  111. package/dist/registry/handles.d.ts +41 -0
  112. package/dist/registry/handles.d.ts.map +1 -0
  113. package/dist/registry/index.d.ts +41 -41
  114. package/dist/registry/index.d.ts.map +1 -1
  115. package/dist/registry/loaders.d.ts +55 -0
  116. package/dist/registry/loaders.d.ts.map +1 -0
  117. package/dist/registry/registry-class.d.ts +157 -0
  118. package/dist/registry/registry-class.d.ts.map +1 -0
  119. package/dist/registry/schema-check.d.ts +14 -0
  120. package/dist/registry/schema-check.d.ts.map +1 -0
  121. package/dist/router/index.d.ts +78 -1
  122. package/dist/router/index.d.ts.map +1 -1
  123. package/dist/router/index.js +1 -1
  124. package/dist/{shell-translations.pt-br-2GWRQRRP.js → shell-translations.pt-br-BM5ZFSKJ.js} +72 -3
  125. package/dist/shell-translations.pt-br-BM5ZFSKJ.js.map +1 -0
  126. package/dist/telemetry/errors.d.ts +87 -0
  127. package/dist/telemetry/errors.d.ts.map +1 -0
  128. package/dist/testing/index.js +4 -1
  129. package/dist/{testing/index.d.ts → testing.d.ts} +5 -3
  130. package/dist/testing.d.ts.map +1 -0
  131. package/dist/types/analytics.d.ts +213 -0
  132. package/dist/types/analytics.d.ts.map +1 -0
  133. package/dist/types/crud.d.ts +37 -3
  134. package/dist/types/crud.d.ts.map +1 -1
  135. package/dist/types/plugins.d.ts +138 -1
  136. package/dist/types/plugins.d.ts.map +1 -1
  137. package/package.json +9 -17
  138. package/dist/blocks/index.d.ts +0 -25
  139. package/dist/blocks/index.d.ts.map +0 -1
  140. package/dist/chunk-5RJVUC6J.js.map +0 -1
  141. package/dist/chunk-L4TLMYGC.js +0 -42
  142. package/dist/chunk-L4TLMYGC.js.map +0 -1
  143. package/dist/chunk-V2MSAUHO.js.map +0 -1
  144. package/dist/chunk-X5THHG6T.js +0 -77
  145. package/dist/chunk-X5THHG6T.js.map +0 -1
  146. package/dist/chunk-Y2C2FBQP.js +0 -35
  147. package/dist/chunk-Y2C2FBQP.js.map +0 -1
  148. package/dist/chunk-YH25Y4FW.js +0 -311
  149. package/dist/chunk-YH25Y4FW.js.map +0 -1
  150. package/dist/phone/index.d.ts.map +0 -1
  151. package/dist/seo/index.d.ts.map +0 -1
  152. package/dist/shell-translations.pt-br-2GWRQRRP.js.map +0 -1
  153. package/dist/tenant/index.d.ts.map +0 -1
  154. package/dist/testing/index.d.ts.map +0 -1
  155. package/src/access/denial.ts +0 -33
  156. package/src/access/engine.test.ts +0 -48
  157. package/src/access/engine.ts +0 -136
  158. package/src/access/index.ts +0 -14
  159. package/src/access/limits.ts +0 -18
  160. package/src/address/index.ts +0 -188
  161. package/src/app/render.tsx +0 -69
  162. package/src/blocks/index.tsx +0 -68
  163. package/src/data/archetype.ts +0 -255
  164. package/src/data/bulk.ts +0 -35
  165. package/src/data/cached.ts +0 -55
  166. package/src/data/count.test.ts +0 -152
  167. package/src/data/count.ts +0 -131
  168. package/src/data/filters.test.ts +0 -70
  169. package/src/data/filters.ts +0 -90
  170. package/src/data/index.ts +0 -11
  171. package/src/data/mock.ts +0 -90
  172. package/src/data/platform-api.ts +0 -209
  173. package/src/data/refresh.ts +0 -115
  174. package/src/data/resolve.ts +0 -126
  175. package/src/data/supabase.ts +0 -230
  176. package/src/data/types.ts +0 -50
  177. package/src/entity/index.ts +0 -2
  178. package/src/entity/registry.ts +0 -49
  179. package/src/events/index.tsx +0 -82
  180. package/src/i18n/index.ts +0 -476
  181. package/src/i18n/shell-translations.en.ts +0 -875
  182. package/src/i18n/shell-translations.pt-br.ts +0 -873
  183. package/src/index.ts +0 -217
  184. package/src/integrations/index.ts +0 -237
  185. package/src/lib/cache.ts +0 -118
  186. package/src/lib/csv.ts +0 -54
  187. package/src/lib/format.ts +0 -58
  188. package/src/manifest/app-manifest.schema.json +0 -676
  189. package/src/manifest/index.ts +0 -518
  190. package/src/manifest/serialize.ts +0 -52
  191. package/src/phone/index.ts +0 -85
  192. package/src/plugin/createSafeDataProvider.ts +0 -30
  193. package/src/plugin/index.ts +0 -1
  194. package/src/plugin/runtime.ts +0 -331
  195. package/src/registry/index.ts +0 -163
  196. package/src/router/history.ts +0 -86
  197. package/src/router/index.ts +0 -29
  198. package/src/runtime/index.ts +0 -15
  199. package/src/runtime/oauth.ts +0 -219
  200. package/src/search/engine.ts +0 -559
  201. package/src/search/index.ts +0 -25
  202. package/src/search/text.ts +0 -206
  203. package/src/search/types.ts +0 -75
  204. package/src/seo/index.ts +0 -128
  205. package/src/tenant/index.ts +0 -38
  206. package/src/testing/index.ts +0 -116
  207. package/src/types/auth.ts +0 -41
  208. package/src/types/billing.ts +0 -39
  209. package/src/types/crud.ts +0 -184
  210. package/src/types/entities.ts +0 -142
  211. package/src/types/entitlements.ts +0 -45
  212. package/src/types/i18n.ts +0 -7
  213. package/src/types/index.ts +0 -11
  214. package/src/types/org.ts +0 -137
  215. package/src/types/payment.ts +0 -44
  216. package/src/types/permissions.ts +0 -32
  217. package/src/types/plugins.ts +0 -480
  218. package/src/types/theme.ts +0 -8
@@ -1,70 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { parseRangeFilter, isFilterObject, matchesRange, applyRangeToQuery } from './filters'
3
-
4
- describe('parseRangeFilter', () => {
5
- it('reads the {from,to} dialect', () => {
6
- expect(parseRangeFilter({ from: 1, to: 5 })).toEqual({ gte: 1, lte: 5 })
7
- })
8
-
9
- it('reads the Mongo dialect the models actually emit', () => {
10
- // The exact filter that answered "não há compromissos hoje" over a booked day.
11
- expect(
12
- parseRangeFilter({ $gte: '2026-07-27T00:00:00Z', $lt: '2026-07-28T00:00:00Z' }),
13
- ).toEqual({ gte: '2026-07-27T00:00:00Z', lt: '2026-07-28T00:00:00Z' })
14
- })
15
-
16
- it('reads bare comparison keys', () => {
17
- expect(parseRangeFilter({ gte: 10, lt: 20 })).toEqual({ gte: 10, lt: 20 })
18
- })
19
-
20
- it('returns null for plain values, so they stay equality filters', () => {
21
- expect(parseRangeFilter('open')).toBeNull()
22
- expect(parseRangeFilter(3)).toBeNull()
23
- expect(parseRangeFilter(null)).toBeNull()
24
- })
25
-
26
- it('returns null for an object with no recognised bound — never a silent match-nothing', () => {
27
- expect(parseRangeFilter({ equals: 'open' })).toBeNull()
28
- expect(isFilterObject({ equals: 'open' })).toBe(true)
29
- })
30
- })
31
-
32
- describe('matchesRange', () => {
33
- const day = { gte: '2026-07-27T00:00:00Z', lt: '2026-07-28T00:00:00Z' }
34
-
35
- it('keeps a timestamp inside the window', () => {
36
- expect(matchesRange('2026-07-27T18:00:00+00:00', day)).toBe(true)
37
- })
38
-
39
- it('drops the bounds it should', () => {
40
- expect(matchesRange('2026-07-28T00:00:00Z', day)).toBe(false)
41
- expect(matchesRange('2026-07-26T23:59:59Z', day)).toBe(false)
42
- })
43
-
44
- it('honours inclusive upper bounds', () => {
45
- expect(matchesRange(5, { gte: 1, lte: 5 })).toBe(true)
46
- expect(matchesRange(5, { gte: 1, lt: 5 })).toBe(false)
47
- })
48
-
49
- it('rejects values that are not comparable instead of letting them through', () => {
50
- expect(matchesRange(null, day)).toBe(false)
51
- expect(matchesRange('não é data', day)).toBe(false)
52
- })
53
- })
54
-
55
- describe('applyRangeToQuery', () => {
56
- it('emits one PostgREST call per bound', () => {
57
- const calls: Array<[string, string, unknown]> = []
58
- const builder = {
59
- gte(c: string, v: unknown) { calls.push(['gte', c, v]); return this },
60
- gt(c: string, v: unknown) { calls.push(['gt', c, v]); return this },
61
- lte(c: string, v: unknown) { calls.push(['lte', c, v]); return this },
62
- lt(c: string, v: unknown) { calls.push(['lt', c, v]); return this },
63
- }
64
- applyRangeToQuery(builder, 'starts_at', { gte: 'a', lt: 'b' })
65
- expect(calls).toEqual([
66
- ['gte', 'starts_at', 'a'],
67
- ['lt', 'starts_at', 'b'],
68
- ])
69
- })
70
- })
@@ -1,90 +0,0 @@
1
- /**
2
- * ONE filter grammar, shared by the providers and by the agent's data tools.
3
- *
4
- * Models express ranges in whatever dialect they were trained on — `{from,to}`,
5
- * `{gte,lte}`, Mongo's `{$gte,$lt}`. When only some of those are recognised the
6
- * unrecognised one falls through to an equality compare against
7
- * "[object Object]", which matches nothing and reads as "there are no
8
- * appointments today" instead of "I did not understand that filter".
9
- */
10
-
11
- export interface RangeFilter {
12
- gte?: unknown
13
- gt?: unknown
14
- lte?: unknown
15
- lt?: unknown
16
- }
17
-
18
- const LOWER_INCLUSIVE = ['from', 'gte', 'start', 'after', 'since', 'min']
19
- const LOWER_EXCLUSIVE = ['gt']
20
- const UPPER_INCLUSIVE = ['to', 'lte', 'end', 'until', 'before', 'max']
21
- const UPPER_EXCLUSIVE = ['lt']
22
-
23
- function pick(source: Record<string, unknown>, names: string[]): unknown {
24
- for (const name of names) {
25
- if (source[name] !== undefined && source[name] !== null) return source[name]
26
- const dollar = `$${name}`
27
- if (source[dollar] !== undefined && source[dollar] !== null) return source[dollar]
28
- }
29
- return undefined
30
- }
31
-
32
- /**
33
- * Parse a filter value into a range, or return null when it is a plain
34
- * (equality) value. An object that looks like a filter but carries no
35
- * recognised bound returns `null` too — callers treat that as "invalid",
36
- * never as "matches nothing".
37
- */
38
- export function parseRangeFilter(value: unknown): RangeFilter | null {
39
- if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
40
- const source = value as Record<string, unknown>
41
- const range: RangeFilter = {}
42
- const gte = pick(source, LOWER_INCLUSIVE)
43
- const gt = pick(source, LOWER_EXCLUSIVE)
44
- const lte = pick(source, UPPER_INCLUSIVE)
45
- const lt = pick(source, UPPER_EXCLUSIVE)
46
- if (gte !== undefined) range.gte = gte
47
- if (gt !== undefined) range.gt = gt
48
- if (lte !== undefined) range.lte = lte
49
- if (lt !== undefined) range.lt = lt
50
- return Object.keys(range).length ? range : null
51
- }
52
-
53
- /** True for any object value — including ones no bound could be read from. */
54
- export function isFilterObject(value: unknown): boolean {
55
- return typeof value === 'object' && value !== null && !Array.isArray(value)
56
- }
57
-
58
- function comparable(value: unknown): number {
59
- if (typeof value === 'number') return value
60
- if (value instanceof Date) return value.getTime()
61
- const ts = Date.parse(String(value))
62
- if (!Number.isNaN(ts)) return ts
63
- return Number(value)
64
- }
65
-
66
- /** Client-side counterpart of the range a provider would push to SQL. */
67
- export function matchesRange(raw: unknown, range: RangeFilter): boolean {
68
- const value = comparable(raw)
69
- if (Number.isNaN(value)) return false
70
- if (range.gte !== undefined && value < comparable(range.gte)) return false
71
- if (range.gt !== undefined && value <= comparable(range.gt)) return false
72
- if (range.lte !== undefined && value > comparable(range.lte)) return false
73
- if (range.lt !== undefined && value >= comparable(range.lt)) return false
74
- return true
75
- }
76
-
77
- /** Apply a range to a PostgREST query builder. */
78
- export function applyRangeToQuery<Q>(
79
- query: Q,
80
- column: string,
81
- range: RangeFilter,
82
- ): Q {
83
- let q = query as Record<string, (c: string, v: unknown) => unknown>
84
- for (const op of ['gte', 'gt', 'lte', 'lt'] as const) {
85
- const bound = range[op]
86
- if (bound === undefined) continue
87
- q = q[op](column, bound) as Record<string, (c: string, v: unknown) => unknown>
88
- }
89
- return q as Q
90
- }
package/src/data/index.ts DELETED
@@ -1,11 +0,0 @@
1
- export { createSupabaseProvider, setGlobalSupabaseClient, getSupabaseClientOptional } from './supabase'
2
- export { countByTenant, invalidateCount } from './count'
3
- export type { CountByTenantOptions } from './count'
4
- export { createFayzApiProvider } from './platform-api'
5
- export { createMockProvider } from './mock'
6
- export { bulkCreate } from './bulk'
7
- export { resolveDataProvider } from './resolve'
8
- export type { DataProvider, CrudQuery, CrudResult, BulkWriteOptions, BulkWriteResult } from './types'
9
- export type { SupabaseProviderConfig } from './supabase'
10
- export type { FayzApiProviderConfig } from './platform-api'
11
- export type { ResolveDataProviderOptions } from './resolve'
package/src/data/mock.ts DELETED
@@ -1,90 +0,0 @@
1
- import type { DataProvider, CrudQuery, CrudResult } from './types'
2
- import type { EntityDef } from '../types/crud'
3
- import { matchesRange, parseRangeFilter } from './filters'
4
-
5
- export function createMockProvider<T extends { id: string; [key: string]: unknown }>(
6
- entityDefOrSearchKeys: EntityDef<T> | string[],
7
- initialData: T[] = [],
8
- ): DataProvider<T> {
9
- let items = [...initialData]
10
-
11
- const searchableKeys = Array.isArray(entityDefOrSearchKeys)
12
- ? entityDefOrSearchKeys
13
- : entityDefOrSearchKeys.fields.filter((f) => f.searchable).map((f) => f.key)
14
-
15
- const defaultSort = Array.isArray(entityDefOrSearchKeys) ? undefined : entityDefOrSearchKeys.defaultSort
16
- const defaultSortDir = Array.isArray(entityDefOrSearchKeys) ? 'asc' : (entityDefOrSearchKeys.defaultSortDir ?? 'asc')
17
-
18
- return {
19
- async list(query: CrudQuery): Promise<CrudResult<T>> {
20
- let filtered = [...items]
21
-
22
- if (query.search && searchableKeys.length > 0) {
23
- const term = query.search.toLowerCase()
24
- filtered = filtered.filter((item) =>
25
- searchableKeys.some((key) => {
26
- const val = item[key]
27
- return typeof val === 'string' && val.toLowerCase().includes(term)
28
- }),
29
- )
30
- }
31
-
32
- // Faceted / dynamic filters — same grammar the SQL providers speak, so a
33
- // date window behaves identically on mock data instead of silently
34
- // matching nothing (a range object is never `===` anything).
35
- if (query.filters) {
36
- for (const [key, val] of Object.entries(query.filters)) {
37
- if (val == null || val === '') continue
38
- const range = parseRangeFilter(val)
39
- filtered = range
40
- ? filtered.filter((item) => matchesRange(item[key], range))
41
- : filtered.filter((item) => item[key] === val)
42
- }
43
- }
44
-
45
- const sortBy = query.sortBy ?? defaultSort
46
- const sortDir = query.sortDir ?? defaultSortDir
47
- if (sortBy) {
48
- filtered.sort((a, b) => {
49
- const av = a[sortBy] ?? ''
50
- const bv = b[sortBy] ?? ''
51
- const cmp = typeof av === 'number' && typeof bv === 'number'
52
- ? av - bv
53
- : String(av).localeCompare(String(bv))
54
- return sortDir === 'desc' ? -cmp : cmp
55
- })
56
- }
57
-
58
- const total = filtered.length
59
- const page = query.page ?? 1
60
- const pageSize = query.pageSize ?? 50
61
- const start = (page - 1) * pageSize
62
- const data = filtered.slice(start, start + pageSize)
63
- return { data, total }
64
- },
65
-
66
- async create(data) {
67
- const now = new Date().toISOString()
68
- const item = {
69
- ...data,
70
- id: crypto.randomUUID(),
71
- tenantId: 'mock-tenant',
72
- createdAt: now,
73
- updatedAt: now,
74
- } as unknown as T
75
- items.unshift(item)
76
- return item
77
- },
78
-
79
- async update(id, data) {
80
- const idx = items.findIndex((i) => i.id === id)
81
- if (idx === -1) throw new Error(`[@fayz-ai/core] Mock provider: item not found: ${id}`)
82
- items[idx] = { ...items[idx], ...data, updatedAt: new Date().toISOString() }
83
- return items[idx]!
84
- },
85
-
86
- async remove(id) {
87
- items = items.filter((i) => i.id !== id)
88
- },
89
- }
90
- }
@@ -1,209 +0,0 @@
1
- import type { CrudQuery, CrudResult, DataProvider } from './types'
2
- import { parseRangeFilter } from './filters'
3
-
4
- type FayzApiFilterOperator = 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'like' | 'ilike' | 'is_null' | 'is_not_null'
5
-
6
- interface FayzApiTableFilter {
7
- column: string
8
- operator: FayzApiFilterOperator
9
- value?: unknown
10
- }
11
-
12
- export interface FayzApiProviderConfig {
13
- baseUrl?: string
14
- projectId?: string
15
- tenantId?: string | (() => string | undefined)
16
- tenantIdColumn?: string | false
17
- entityKey?: string
18
- table?: string
19
- schema?: string
20
- idColumn?: string
21
- searchColumns?: string[]
22
- runtimeToken?: string | (() => string | undefined | Promise<string | undefined>)
23
- headers?: () => HeadersInit | Promise<HeadersInit>
24
- fetcher?: typeof fetch
25
- }
26
-
27
- function resolveTenantId(config?: FayzApiProviderConfig): string | undefined {
28
- const tenantId = config?.tenantId
29
- return typeof tenantId === 'function' ? tenantId() : tenantId
30
- }
31
-
32
- async function resolveRuntimeToken(config?: FayzApiProviderConfig): Promise<string | undefined> {
33
- const runtimeToken = config?.runtimeToken
34
- return typeof runtimeToken === 'function' ? runtimeToken() : runtimeToken
35
- }
36
-
37
- function trimTrailingSlash(value: string): string {
38
- return value.replace(/\/+$/, '')
39
- }
40
-
41
- function appendQuery(
42
- url: URL,
43
- query: CrudQuery,
44
- tenantId?: string,
45
- tenantIdColumn = 'tenant_id',
46
- searchColumns: string[] = [],
47
- ): void {
48
- const filters = normalizeFilters(query.filters)
49
- if (query.search && searchColumns[0]) {
50
- filters.push({ column: searchColumns[0], operator: 'ilike', value: `%${query.search}%` })
51
- }
52
- if (query.sortBy) url.searchParams.set('sortColumn', query.sortBy)
53
- if (query.sortDir) url.searchParams.set('sortDirection', query.sortDir)
54
- if (query.page != null) url.searchParams.set('page', String(query.page))
55
- if (query.pageSize != null) url.searchParams.set('limit', String(query.pageSize))
56
- if (tenantId) filters.push({ column: tenantIdColumn, operator: 'eq', value: tenantId })
57
- if (filters.length > 0) url.searchParams.set('filters', JSON.stringify(filters))
58
- }
59
-
60
- async function parseResponse<T>(response: Response): Promise<T> {
61
- if (!response.ok) {
62
- const text = await response.text().catch(() => '')
63
- throw new Error(`[@fayz-ai/core] Fayz API request failed (${response.status}): ${text || response.statusText}`)
64
- }
65
- return response.json() as Promise<T>
66
- }
67
-
68
- function isFilterOperator(value: unknown): value is FayzApiFilterOperator {
69
- return typeof value === 'string' && [
70
- 'eq',
71
- 'neq',
72
- 'gt',
73
- 'lt',
74
- 'gte',
75
- 'lte',
76
- 'like',
77
- 'ilike',
78
- 'is_null',
79
- 'is_not_null',
80
- ].includes(value)
81
- }
82
-
83
- function isRecord(value: unknown): value is Record<string, unknown> {
84
- return typeof value === 'object' && value !== null && !Array.isArray(value)
85
- }
86
-
87
- function normalizeFilters(filters?: Record<string, unknown>): FayzApiTableFilter[] {
88
- if (!filters) return []
89
- return Object.entries(filters).flatMap(([column, raw]): FayzApiTableFilter[] => {
90
- if (isRecord(raw) && isFilterOperator(raw.operator)) {
91
- return [{ column, operator: raw.operator, value: raw.value }]
92
- }
93
- // A range is TWO bounds on one column — sending it as a single `eq` filter
94
- // compared the column against an object and matched nothing.
95
- const range = parseRangeFilter(raw)
96
- if (range) {
97
- return (['gte', 'gt', 'lte', 'lt'] as const)
98
- .filter((op) => range[op] !== undefined)
99
- .map((op) => ({ column, operator: op, value: range[op] }))
100
- }
101
- return [{ column, operator: 'eq', value: raw }]
102
- })
103
- }
104
-
105
- export function createFayzApiProvider<T extends { id: string }>(
106
- entityOrTable: string,
107
- config: FayzApiProviderConfig = {},
108
- ): DataProvider<T> {
109
- const fetcher = config.fetcher ?? fetch
110
- const baseUrl = trimTrailingSlash(config.baseUrl ?? '')
111
- const projectId = config.projectId
112
- const table = config.table ?? entityOrTable
113
- const idColumn = config.idColumn ?? 'id'
114
-
115
- async function headers(runtimeToken?: string): Promise<HeadersInit> {
116
- return {
117
- 'Content-Type': 'application/json',
118
- ...(config.headers ? await config.headers() : {}),
119
- ...(runtimeToken ? { Authorization: `Bearer ${runtimeToken}` } : {}),
120
- }
121
- }
122
-
123
- function tableRowsUrl(runtime = false): URL {
124
- const prefix = baseUrl || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost')
125
- const encodedProjectId = encodeURIComponent(projectId ?? 'current')
126
- const encodedTable = encodeURIComponent(table)
127
- const route = runtime
128
- ? `/api/v1/runtime/projects/${encodedProjectId}/database/tables/${encodedTable}/rows`
129
- : `/api/projects/${encodedProjectId}/database/tables/${encodedTable}/rows`
130
- const url = new URL(route, prefix)
131
- if (config.schema) url.searchParams.set('schema', config.schema)
132
- return url
133
- }
134
-
135
- function withTenant(data: Record<string, unknown>, runtime = false): Record<string, unknown> {
136
- if (runtime) return data
137
- const tenantId = resolveTenantId(config)
138
- if (!tenantId || config.tenantIdColumn === false) return data
139
- return { ...data, [config.tenantIdColumn ?? 'tenant_id']: tenantId }
140
- }
141
-
142
- function primaryKeys(id: string): Record<string, unknown> {
143
- return { [idColumn]: id }
144
- }
145
-
146
- async function listRows(query: CrudQuery): Promise<CrudResult<T>> {
147
- const runtimeToken = await resolveRuntimeToken(config)
148
- const runtime = Boolean(runtimeToken)
149
- const url = tableRowsUrl(runtime)
150
- appendQuery(
151
- url,
152
- query,
153
- runtime || config.tenantIdColumn === false ? undefined : resolveTenantId(config),
154
- config.tenantIdColumn || undefined,
155
- config.searchColumns,
156
- )
157
- const result = await parseResponse<{ rows: T[]; total: number }>(await fetcher(url, { headers: await headers(runtimeToken) }))
158
- return { data: result.rows, total: result.total }
159
- }
160
-
161
- async function assertTenantScopedRowExists(id: string, runtime = false): Promise<void> {
162
- if (runtime) return
163
- const tenantId = config.tenantIdColumn === false ? undefined : resolveTenantId(config)
164
- if (!tenantId) return
165
-
166
- const result = await listRows({ filters: { [idColumn]: id }, page: 1, pageSize: 1 })
167
- if (result.data.length === 0) {
168
- throw new Error(`[@fayz-ai/core] Row "${id}" was not found for the active tenant.`)
169
- }
170
- }
171
-
172
- return {
173
- async list(query: CrudQuery): Promise<CrudResult<T>> {
174
- return listRows(query)
175
- },
176
-
177
- async create(data) {
178
- const runtimeToken = await resolveRuntimeToken(config)
179
- const runtime = Boolean(runtimeToken)
180
- return parseResponse<T>(await fetcher(tableRowsUrl(runtime), {
181
- method: 'POST',
182
- headers: await headers(runtimeToken),
183
- body: JSON.stringify(withTenant(data as Record<string, unknown>, runtime)),
184
- }))
185
- },
186
-
187
- async update(id, data) {
188
- const runtimeToken = await resolveRuntimeToken(config)
189
- const runtime = Boolean(runtimeToken)
190
- await assertTenantScopedRowExists(id, runtime)
191
- return parseResponse<T>(await fetcher(tableRowsUrl(runtime), {
192
- method: 'PATCH',
193
- headers: await headers(runtimeToken),
194
- body: JSON.stringify({ primaryKeys: primaryKeys(id), data }),
195
- }))
196
- },
197
-
198
- async remove(id) {
199
- const runtimeToken = await resolveRuntimeToken(config)
200
- const runtime = Boolean(runtimeToken)
201
- await assertTenantScopedRowExists(id, runtime)
202
- await parseResponse<{ deletedCount: number }>(await fetcher(tableRowsUrl(runtime), {
203
- method: 'DELETE',
204
- headers: await headers(runtimeToken),
205
- body: JSON.stringify({ rows: [primaryKeys(id)] }),
206
- }))
207
- },
208
- }
209
- }
@@ -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
- }