@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.
- package/README.md +2 -2
- package/dist/access/index.js.map +1 -1
- package/dist/chunk-BEGXYHS4.js +566 -0
- package/dist/chunk-BEGXYHS4.js.map +1 -0
- package/dist/chunk-ED6NBH6U.js +348 -0
- package/dist/chunk-ED6NBH6U.js.map +1 -0
- package/dist/chunk-EWKX4VUM.js +429 -0
- package/dist/chunk-EWKX4VUM.js.map +1 -0
- package/dist/chunk-LARZWDHA.js +319 -0
- package/dist/chunk-LARZWDHA.js.map +1 -0
- package/dist/chunk-OXQGRJZT.js +125 -0
- package/dist/chunk-OXQGRJZT.js.map +1 -0
- package/dist/chunk-RRYFXUQX.js +1247 -0
- package/dist/chunk-RRYFXUQX.js.map +1 -0
- package/dist/{chunk-5RJVUC6J.js → chunk-TDI6F6PT.js} +373 -25
- package/dist/chunk-TDI6F6PT.js.map +1 -0
- package/dist/{chunk-V2MSAUHO.js → chunk-UVKH6RDX.js} +211 -335
- package/dist/chunk-UVKH6RDX.js.map +1 -0
- package/dist/chunk-V53GR4UT.js +289 -0
- package/dist/chunk-V53GR4UT.js.map +1 -0
- package/dist/connector-runtime/index.d.ts +15 -0
- package/dist/connector-runtime/index.d.ts.map +1 -0
- package/dist/connector-runtime/index.js +3 -0
- package/dist/connector-runtime/index.js.map +1 -0
- package/dist/data/archetype.d.ts.map +1 -1
- package/dist/data/backend-env.d.ts +38 -0
- package/dist/data/backend-env.d.ts.map +1 -0
- package/dist/data/bulk.d.ts +24 -0
- package/dist/data/bulk.d.ts.map +1 -1
- package/dist/data/cached.d.ts.map +1 -1
- package/dist/data/custom-fields.d.ts +5 -0
- package/dist/data/custom-fields.d.ts.map +1 -0
- package/dist/data/index.d.ts +2 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +3 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/platform-api.d.ts.map +1 -1
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +8 -0
- package/dist/data/types.d.ts.map +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/fields/store.d.ts +45 -0
- package/dist/fields/store.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/i18n/shell-translations.en.d.ts.map +1 -1
- package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +543 -56
- package/dist/index.js.map +1 -1
- package/dist/integrations/events.d.ts +7 -0
- package/dist/integrations/events.d.ts.map +1 -0
- package/dist/integrations/index.d.ts +533 -29
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +3 -1
- package/dist/integrations/permissions.d.ts +132 -0
- package/dist/integrations/permissions.d.ts.map +1 -0
- package/dist/integrations/request.d.ts +170 -0
- package/dist/integrations/request.d.ts.map +1 -0
- package/dist/integrations/runtime-token.d.ts +33 -0
- package/dist/integrations/runtime-token.d.ts.map +1 -0
- package/dist/integrations/runtime.d.ts +398 -0
- package/dist/integrations/runtime.d.ts.map +1 -0
- package/dist/integrations/settings.d.ts +102 -0
- package/dist/integrations/settings.d.ts.map +1 -0
- package/dist/integrations/store.d.ts +145 -0
- package/dist/integrations/store.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 +3 -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 +179 -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/{shell-translations.pt-br-2GWRQRRP.js → shell-translations.pt-br-BM5ZFSKJ.js} +72 -3
- package/dist/shell-translations.pt-br-BM5ZFSKJ.js.map +1 -0
- package/dist/telemetry/errors.d.ts +87 -0
- package/dist/telemetry/errors.d.ts.map +1 -0
- package/dist/testing/index.js +4 -1
- package/dist/{testing/index.d.ts → testing.d.ts} +5 -3
- package/dist/testing.d.ts.map +1 -0
- package/dist/types/analytics.d.ts +213 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/crud.d.ts +37 -3
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +138 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +9 -17
- 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 +0 -42
- package/dist/chunk-L4TLMYGC.js.map +0 -1
- package/dist/chunk-V2MSAUHO.js.map +0 -1
- package/dist/chunk-X5THHG6T.js +0 -77
- 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/phone/index.d.ts.map +0 -1
- package/dist/seo/index.d.ts.map +0 -1
- package/dist/shell-translations.pt-br-2GWRQRRP.js.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/archetype.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import type { DataProvider, CrudQuery, CrudResult } from './types'
|
|
2
|
-
import { getSupabaseClientOptional } from './supabase'
|
|
3
|
-
import { applyRangeToQuery, isFilterObject, parseRangeFilter } from './filters'
|
|
4
|
-
import type { EntityArchetype } from '../types/entities'
|
|
5
|
-
|
|
6
|
-
// Archetypes that have a `kind` discriminator column
|
|
7
|
-
const HAS_KIND = new Set<EntityArchetype>(['person', 'category', 'order', 'transaction', 'schedule', 'location'])
|
|
8
|
-
|
|
9
|
-
const ARCHETYPE_CONFIG: Record<EntityArchetype, { table: string; fkColumn: string }> = {
|
|
10
|
-
person: { table: 'people', fkColumn: 'person_id' },
|
|
11
|
-
category: { table: 'categories', fkColumn: 'category_id' },
|
|
12
|
-
product: { table: 'products', fkColumn: 'product_id' },
|
|
13
|
-
service: { table: 'services', fkColumn: 'service_id' },
|
|
14
|
-
location: { table: 'locations', fkColumn: 'location_id' },
|
|
15
|
-
order: { table: 'orders', fkColumn: 'order_id' },
|
|
16
|
-
transaction: { table: 'transactions', fkColumn: 'transaction_id' },
|
|
17
|
-
|
|
18
|
-
schedule: { table: 'schedules', fkColumn: 'schedule_id' },
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const ARCHETYPE_COLUMNS: Record<EntityArchetype, Set<string>> = {
|
|
22
|
-
person: new Set(['name', 'email', 'phone', 'document_number', 'avatar_url', 'date_of_birth', 'address', 'city', 'state', 'country', 'postal_code', 'tags', 'is_active', 'notes', 'metadata']),
|
|
23
|
-
category: new Set(['name', 'slug', 'parent_id', 'icon', 'color', 'sort_order', 'is_active', 'metadata']),
|
|
24
|
-
product: new Set(['name', 'description', 'sku', 'price', 'cost', 'currency', 'unit', 'image_url', 'stock', 'min_stock', 'status', 'is_active', 'tags', 'metadata', 'category_id']),
|
|
25
|
-
service: new Set(['name', 'description', 'price', 'cost', 'currency', 'duration_minutes', 'image_url', 'status', 'is_active', 'tags', 'metadata', 'category_id']),
|
|
26
|
-
location: new Set(['name', 'email', 'phone', 'address', 'city', 'state', 'country', 'postal_code', 'is_headquarters', 'is_active', 'tags', 'notes', 'metadata']),
|
|
27
|
-
order: new Set(['reference_number', 'status', 'party_id', 'assignee_id', 'location_id', 'subtotal', 'discount', 'tax', 'total', 'currency', 'due_at', 'completed_at', 'notes', 'tags', 'metadata']),
|
|
28
|
-
transaction: new Set(['order_id', 'party_id', 'amount', 'currency', 'payment_method', 'reference', 'status', 'transacted_at', 'notes', 'metadata']),
|
|
29
|
-
|
|
30
|
-
schedule: new Set(['assignee_id', 'location_id', 'day_of_week', 'specific_date', 'starts_at', 'ends_at', 'is_active', 'metadata']),
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function camelToSnake(str: string): string {
|
|
34
|
-
return str.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase())
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function snakeToCamel(str: string): string {
|
|
38
|
-
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function mapRow<T>(row: Record<string, unknown>): T {
|
|
42
|
-
const result: Record<string, unknown> = {}
|
|
43
|
-
for (const [key, value] of Object.entries(row)) {
|
|
44
|
-
result[snakeToCamel(key)] = value
|
|
45
|
-
}
|
|
46
|
-
return result as T
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface ArchetypeProviderConfig {
|
|
50
|
-
archetype: EntityArchetype
|
|
51
|
-
archetypeKind: string
|
|
52
|
-
projectTable: string
|
|
53
|
-
tenantId: () => string | undefined
|
|
54
|
-
searchColumns?: string[]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function createArchetypeProvider<T extends { id: string }>(
|
|
58
|
-
config: ArchetypeProviderConfig,
|
|
59
|
-
): DataProvider<T> {
|
|
60
|
-
const ac = ARCHETYPE_CONFIG[config.archetype]
|
|
61
|
-
const archetypeColumns = ARCHETYPE_COLUMNS[config.archetype]
|
|
62
|
-
const { fkColumn } = ac
|
|
63
|
-
|
|
64
|
-
function getClient() {
|
|
65
|
-
const supabase = getSupabaseClientOptional() as any
|
|
66
|
-
if (!supabase) throw new Error('Supabase client not available')
|
|
67
|
-
return supabase
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Core v1 lives in PUBLIC — the archetype base tables and the public extension
|
|
71
|
-
// tables share one client/schema. `coreClient()` is kept as a thin alias so the
|
|
72
|
-
// core/extension split below still reads clearly at each call site.
|
|
73
|
-
function coreClient() {
|
|
74
|
-
return getClient()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function splitFields(data: Record<string, unknown>): { archetypeData: Record<string, unknown>; projectData: Record<string, unknown> } {
|
|
78
|
-
const archetypeData: Record<string, unknown> = {}
|
|
79
|
-
const projectData: Record<string, unknown> = {}
|
|
80
|
-
for (const [key, value] of Object.entries(data)) {
|
|
81
|
-
if (['id', 'createdAt', 'updatedAt', 'tenantId', fkColumn].includes(key)) continue
|
|
82
|
-
const snakeKey = camelToSnake(key)
|
|
83
|
-
if (archetypeColumns.has(snakeKey)) {
|
|
84
|
-
archetypeData[snakeKey] = value
|
|
85
|
-
} else {
|
|
86
|
-
projectData[snakeKey] = value
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return { archetypeData, projectData }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// View name convention: v_{shortName} — maps project table to its view
|
|
93
|
-
const VIEW_MAP: Record<string, string> = {
|
|
94
|
-
clients: 'v_clients',
|
|
95
|
-
staff_members: 'v_staff',
|
|
96
|
-
}
|
|
97
|
-
const viewName = VIEW_MAP[config.projectTable] ?? `v_${config.projectTable}`
|
|
98
|
-
|
|
99
|
-
// Pure-archetype entity: the project table IS the archetype base (e.g. a
|
|
100
|
-
// customers list configured with `table: 'people'`, no separate public
|
|
101
|
-
// extension table). Read/write public.<base> directly — there is no public
|
|
102
|
-
// extension row and no v_ view to join. Extension-table entities (clients,
|
|
103
|
-
// staff_members, …) are unaffected.
|
|
104
|
-
const isPure = config.projectTable === ac.table
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
async list(query: CrudQuery): Promise<CrudResult<T>> {
|
|
108
|
-
const tenantId = config.tenantId()
|
|
109
|
-
if (!tenantId) return { data: [], total: 0 }
|
|
110
|
-
|
|
111
|
-
// Pure archetype → read public.<base> directly (filtered by kind);
|
|
112
|
-
// otherwise the v_ view (JOINs public extension with the public archetype).
|
|
113
|
-
// 'none' skips PostgREST's count aggregate entirely — see CrudQuery.countMode.
|
|
114
|
-
const countOpts = query.countMode === 'none' ? undefined : { count: 'exact' as const }
|
|
115
|
-
let q = isPure
|
|
116
|
-
? coreClient().from(ac.table).select('*', countOpts).eq('tenant_id', tenantId)
|
|
117
|
-
: getClient().from(viewName).select('*', countOpts).eq('tenant_id', tenantId)
|
|
118
|
-
|
|
119
|
-
if (isPure && HAS_KIND.has(config.archetype)) {
|
|
120
|
-
q = q.eq('kind', config.archetypeKind)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Faceted filters — equality, or real SQL bounds for a range object.
|
|
124
|
-
// Silently dropping them made every caller post-filter one page of rows,
|
|
125
|
-
// which reads as "no records match" whenever the match sits past it.
|
|
126
|
-
if (query.filters) {
|
|
127
|
-
for (const [key, val] of Object.entries(query.filters)) {
|
|
128
|
-
if (val == null || val === '') continue
|
|
129
|
-
const col = camelToSnake(key)
|
|
130
|
-
const range = parseRangeFilter(val)
|
|
131
|
-
if (range) {
|
|
132
|
-
q = applyRangeToQuery(q, col, range)
|
|
133
|
-
continue
|
|
134
|
-
}
|
|
135
|
-
if (isFilterObject(val)) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`[@fayz-ai/core] Filter "${key}" is an object with no recognised bound. ` +
|
|
138
|
-
'Use {from,to} / {gte,lte,gt,lt} for ranges, or a plain value for equality.',
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
q = q.eq(col, val)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Search
|
|
146
|
-
if (query.search && config.searchColumns && config.searchColumns.length > 0) {
|
|
147
|
-
const term = `%${query.search}%`
|
|
148
|
-
const orClauses = config.searchColumns
|
|
149
|
-
.map((col) => camelToSnake(col))
|
|
150
|
-
.map((col) => `${col}.ilike.${term}`)
|
|
151
|
-
if (orClauses.length > 0) {
|
|
152
|
-
q = q.or(orClauses.join(','))
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Sort
|
|
157
|
-
if (query.sortBy) {
|
|
158
|
-
q = q.order(camelToSnake(query.sortBy), { ascending: query.sortDir !== 'desc' })
|
|
159
|
-
} else {
|
|
160
|
-
q = q.order('created_at', { ascending: false })
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Pagination
|
|
164
|
-
const page = query.page ?? 1
|
|
165
|
-
const pageSize = query.pageSize ?? 50
|
|
166
|
-
const from = (page - 1) * pageSize
|
|
167
|
-
q = q.range(from, from + pageSize - 1)
|
|
168
|
-
|
|
169
|
-
const { data, error, count } = await q
|
|
170
|
-
if (error) throw error
|
|
171
|
-
|
|
172
|
-
const rows = (data ?? []).map((row: Record<string, unknown>) => mapRow<T>(row))
|
|
173
|
-
return { data: rows, total: count ?? 0 }
|
|
174
|
-
},
|
|
175
|
-
|
|
176
|
-
async create(data) {
|
|
177
|
-
const tenantId = config.tenantId()
|
|
178
|
-
if (!tenantId) throw new Error('No tenant selected')
|
|
179
|
-
|
|
180
|
-
const { archetypeData, projectData } = splitFields(data as Record<string, unknown>)
|
|
181
|
-
|
|
182
|
-
// 1. Insert into archetype table
|
|
183
|
-
archetypeData.tenant_id = tenantId
|
|
184
|
-
if (HAS_KIND.has(config.archetype)) {
|
|
185
|
-
archetypeData.kind = config.archetypeKind
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const { data: archetypeRow, error: archetypeError } = await coreClient()
|
|
189
|
-
.from(ac.table)
|
|
190
|
-
.insert(archetypeData)
|
|
191
|
-
.select()
|
|
192
|
-
.single()
|
|
193
|
-
|
|
194
|
-
if (archetypeError) throw archetypeError
|
|
195
|
-
const archetypeId = (archetypeRow as any).id
|
|
196
|
-
|
|
197
|
-
// 2. Insert into project table with archetype FK as PK.
|
|
198
|
-
// Pure-archetype entities have no extension table — skip this step.
|
|
199
|
-
if (!isPure) {
|
|
200
|
-
projectData[fkColumn] = archetypeId
|
|
201
|
-
projectData.tenant_id = tenantId
|
|
202
|
-
|
|
203
|
-
const { error: projectError } = await getClient()
|
|
204
|
-
.from(config.projectTable)
|
|
205
|
-
.insert(projectData)
|
|
206
|
-
|
|
207
|
-
if (projectError) {
|
|
208
|
-
await coreClient().from(ac.table).delete().eq('id', archetypeId)
|
|
209
|
-
throw projectError
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return { id: archetypeId, ...archetypeRow, ...(isPure ? {} : projectData) } as unknown as T
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
async update(id, data) {
|
|
217
|
-
const { archetypeData, projectData } = splitFields(data as Record<string, unknown>)
|
|
218
|
-
|
|
219
|
-
if (Object.keys(archetypeData).length > 0) {
|
|
220
|
-
const { error } = await coreClient()
|
|
221
|
-
.from(ac.table)
|
|
222
|
-
.update(archetypeData)
|
|
223
|
-
.eq('id', id)
|
|
224
|
-
if (error) throw error
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!isPure && Object.keys(projectData).length > 0) {
|
|
228
|
-
const { error } = await getClient()
|
|
229
|
-
.from(config.projectTable)
|
|
230
|
-
.update(projectData)
|
|
231
|
-
.eq(fkColumn, id)
|
|
232
|
-
if (error) throw error
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Re-fetch (archetype only for pure entities; otherwise join the extension)
|
|
236
|
-
const { data: archetypeRow } = await coreClient().from(ac.table).select('*').eq('id', id).single()
|
|
237
|
-
if (isPure) {
|
|
238
|
-
return mapRow<T>({ ...archetypeRow, id } as Record<string, unknown>)
|
|
239
|
-
}
|
|
240
|
-
const { data: projectRow } = await getClient().from(config.projectTable).select('*').eq(fkColumn, id).single()
|
|
241
|
-
|
|
242
|
-
const flat = { ...archetypeRow, ...projectRow, id }
|
|
243
|
-
delete (flat as any)[fkColumn]
|
|
244
|
-
return mapRow<T>(flat as Record<string, unknown>)
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
async remove(id) {
|
|
248
|
-
const { error } = await coreClient()
|
|
249
|
-
.from(ac.table)
|
|
250
|
-
.delete()
|
|
251
|
-
.eq('id', id)
|
|
252
|
-
if (error) throw error
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
}
|
package/src/data/bulk.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { BulkWriteOptions, BulkWriteResult, DataProvider } from './types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Write many rows through whatever the provider supports.
|
|
5
|
-
*
|
|
6
|
-
* `createMany` when it exists (one round-trip per chunk), otherwise a sequential
|
|
7
|
-
* `create` loop. Callers never branch: a mock provider and a Supabase table
|
|
8
|
-
* import the same way, only slower.
|
|
9
|
-
*
|
|
10
|
-
* Never all-or-nothing. A 900-row client list will contain a malformed phone
|
|
11
|
-
* and a duplicate e-mail, and a single transaction would throw away the other
|
|
12
|
-
* 898 — plus a statement that large risks the timeout. Failures are collected
|
|
13
|
-
* with their row index so the caller can hand them back for correction.
|
|
14
|
-
*/
|
|
15
|
-
export async function bulkCreate<T extends { id: string }>(
|
|
16
|
-
provider: DataProvider<T>,
|
|
17
|
-
rows: Array<Omit<T, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'>>,
|
|
18
|
-
options?: BulkWriteOptions,
|
|
19
|
-
): Promise<BulkWriteResult> {
|
|
20
|
-
if (!rows.length) return { created: 0, failed: [] }
|
|
21
|
-
if (provider.createMany) return provider.createMany(rows, options)
|
|
22
|
-
|
|
23
|
-
let created = 0
|
|
24
|
-
const failed: Array<{ index: number; message: string }> = []
|
|
25
|
-
for (let i = 0; i < rows.length; i++) {
|
|
26
|
-
try {
|
|
27
|
-
await provider.create(rows[i])
|
|
28
|
-
created += 1
|
|
29
|
-
} catch (error) {
|
|
30
|
-
failed.push({ index: i, message: error instanceof Error ? error.message : String(error) })
|
|
31
|
-
}
|
|
32
|
-
options?.onProgress?.(i + 1, rows.length)
|
|
33
|
-
}
|
|
34
|
-
return { created, failed }
|
|
35
|
-
}
|
package/src/data/cached.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { DataProvider, CrudQuery, CrudResult } from './types'
|
|
2
|
-
import { globalCache, stableKey } from '../lib/cache'
|
|
3
|
-
|
|
4
|
-
export interface ProviderCacheOptions {
|
|
5
|
-
/** Table/entity name — used as cache key namespace */
|
|
6
|
-
table: string
|
|
7
|
-
/** Dynamic tenant ID resolver */
|
|
8
|
-
tenantId: () => string | undefined
|
|
9
|
-
/** Override default TTL for this entity (ms) */
|
|
10
|
-
ttl?: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Wraps a DataProvider with transparent read caching + mutation invalidation.
|
|
15
|
-
*
|
|
16
|
-
* - `list()` results are cached by query params
|
|
17
|
-
* - `create()`, `update()`, `remove()` invalidate all cached entries for the table
|
|
18
|
-
*/
|
|
19
|
-
export function withCache<T extends { id: string }>(
|
|
20
|
-
provider: DataProvider<T>,
|
|
21
|
-
options: ProviderCacheOptions,
|
|
22
|
-
): DataProvider<T> {
|
|
23
|
-
function prefix(): string {
|
|
24
|
-
return `${options.tenantId() ?? '_'}:${options.table}`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
async list(query: CrudQuery): Promise<CrudResult<T>> {
|
|
29
|
-
const key = `${prefix()}:${stableKey(query)}`
|
|
30
|
-
const cached = globalCache.get<CrudResult<T>>(key)
|
|
31
|
-
if (cached) return cached
|
|
32
|
-
|
|
33
|
-
const result = await provider.list(query)
|
|
34
|
-
globalCache.set(key, result, options.ttl)
|
|
35
|
-
return result
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
async create(data) {
|
|
39
|
-
const result = await provider.create(data)
|
|
40
|
-
globalCache.invalidate(prefix())
|
|
41
|
-
return result
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
async update(id, data) {
|
|
45
|
-
const result = await provider.update(id, data)
|
|
46
|
-
globalCache.invalidate(prefix())
|
|
47
|
-
return result
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
async remove(id) {
|
|
51
|
-
await provider.remove(id)
|
|
52
|
-
globalCache.invalidate(prefix())
|
|
53
|
-
},
|
|
54
|
-
}
|
|
55
|
-
}
|
package/src/data/count.test.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
-
import { countByTenant, invalidateCount } from './count'
|
|
3
|
-
import { setGlobalSupabaseClient } from './supabase'
|
|
4
|
-
import { setActiveTenantId } from '../tenant'
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Mock Supabase client — a thenable query builder that records .eq/.gte calls.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
interface MockClient {
|
|
11
|
-
client: unknown
|
|
12
|
-
selectCalls: number
|
|
13
|
-
lastEq: Array<[string, unknown]>
|
|
14
|
-
lastGte: Array<[string, unknown]>
|
|
15
|
-
setCount: (n: number, error?: unknown) => void
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function makeClient(initialCount = 0): MockClient {
|
|
19
|
-
const state = { count: initialCount as number, error: null as unknown }
|
|
20
|
-
const record: MockClient = {
|
|
21
|
-
client: null,
|
|
22
|
-
selectCalls: 0,
|
|
23
|
-
lastEq: [],
|
|
24
|
-
lastGte: [],
|
|
25
|
-
setCount: (n, error = null) => {
|
|
26
|
-
state.count = n
|
|
27
|
-
state.error = error
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const query: Record<string, unknown> = {
|
|
32
|
-
eq: (col: string, val: unknown) => {
|
|
33
|
-
record.lastEq.push([col, val])
|
|
34
|
-
return query
|
|
35
|
-
},
|
|
36
|
-
gte: (col: string, val: unknown) => {
|
|
37
|
-
record.lastGte.push([col, val])
|
|
38
|
-
return query
|
|
39
|
-
},
|
|
40
|
-
then: (resolve: (r: { count: number; error: unknown }) => void) =>
|
|
41
|
-
resolve({ count: state.count, error: state.error }),
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
record.client = {
|
|
45
|
-
from: () => ({
|
|
46
|
-
select: (_cols: string, _opts: unknown) => {
|
|
47
|
-
record.selectCalls++
|
|
48
|
-
return query
|
|
49
|
-
},
|
|
50
|
-
}),
|
|
51
|
-
}
|
|
52
|
-
return record
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
describe('countByTenant', () => {
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
invalidateCount() // clear all cached counts
|
|
58
|
-
setGlobalSupabaseClient(null as unknown as object)
|
|
59
|
-
setActiveTenantId('tenant-1')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('returns 0 in mock mode (no supabase client) — never blocks', async () => {
|
|
63
|
-
setGlobalSupabaseClient(null as unknown as object)
|
|
64
|
-
expect(await countByTenant('clients')).toBe(0)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('returns 0 when no tenant is resolvable', async () => {
|
|
68
|
-
setActiveTenantId(undefined)
|
|
69
|
-
const m = makeClient(5)
|
|
70
|
-
setGlobalSupabaseClient(m.client as object)
|
|
71
|
-
expect(await countByTenant('clients')).toBe(0)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('counts tenant-scoped rows via head:true', async () => {
|
|
75
|
-
const m = makeClient(42)
|
|
76
|
-
setGlobalSupabaseClient(m.client as object)
|
|
77
|
-
expect(await countByTenant('clients')).toBe(42)
|
|
78
|
-
expect(m.lastEq).toContainEqual(['tenant_id', 'tenant-1'])
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('caches within the TTL (second read does not hit the DB)', async () => {
|
|
82
|
-
const m = makeClient(10)
|
|
83
|
-
setGlobalSupabaseClient(m.client as object)
|
|
84
|
-
await countByTenant('clients')
|
|
85
|
-
await countByTenant('clients')
|
|
86
|
-
expect(m.selectCalls).toBe(1)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('fresh:true bypasses the cache', async () => {
|
|
90
|
-
const m = makeClient(10)
|
|
91
|
-
setGlobalSupabaseClient(m.client as object)
|
|
92
|
-
await countByTenant('clients')
|
|
93
|
-
await countByTenant('clients', { fresh: true })
|
|
94
|
-
expect(m.selectCalls).toBe(2)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('invalidateCount(table) drops the cached value', async () => {
|
|
98
|
-
const m = makeClient(10)
|
|
99
|
-
setGlobalSupabaseClient(m.client as object)
|
|
100
|
-
await countByTenant('clients')
|
|
101
|
-
invalidateCount('clients')
|
|
102
|
-
await countByTenant('clients')
|
|
103
|
-
expect(m.selectCalls).toBe(2)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('separate keys (kind/period) are cached independently', async () => {
|
|
107
|
-
const m = makeClient(3)
|
|
108
|
-
setGlobalSupabaseClient(m.client as object)
|
|
109
|
-
await countByTenant('bookings', { period: 'month' })
|
|
110
|
-
await countByTenant('bookings', { period: 'total' })
|
|
111
|
-
expect(m.selectCalls).toBe(2)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('applies kind filter and month window', async () => {
|
|
115
|
-
const m = makeClient(7)
|
|
116
|
-
setGlobalSupabaseClient(m.client as object)
|
|
117
|
-
await countByTenant('items', { kind: 'product', period: 'month' })
|
|
118
|
-
expect(m.lastEq).toContainEqual(['kind', 'product'])
|
|
119
|
-
expect(m.lastGte.some(([col]) => col === 'created_at')).toBe(true)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('fails open (returns 0) when the query errors', async () => {
|
|
123
|
-
const m = makeClient(0)
|
|
124
|
-
m.setCount(0, new Error('boom'))
|
|
125
|
-
setGlobalSupabaseClient(m.client as object)
|
|
126
|
-
expect(await countByTenant('clients')).toBe(0)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('fails open (returns 0) instead of hanging when the query never settles', async () => {
|
|
130
|
-
// A cold-boot query can be issued before the auth session is ready and then
|
|
131
|
-
// never resolve. The guard awaiting this must not hang the UI forever.
|
|
132
|
-
const client = {
|
|
133
|
-
from: () => ({
|
|
134
|
-
select: () => ({
|
|
135
|
-
eq: () => ({
|
|
136
|
-
eq: () => neverSettles,
|
|
137
|
-
gte: () => neverSettles,
|
|
138
|
-
then: neverSettles.then.bind(neverSettles),
|
|
139
|
-
}),
|
|
140
|
-
}),
|
|
141
|
-
}),
|
|
142
|
-
}
|
|
143
|
-
const neverSettles = { then: () => {} } as unknown as Promise<never>
|
|
144
|
-
setGlobalSupabaseClient(client as object)
|
|
145
|
-
|
|
146
|
-
vi.useFakeTimers()
|
|
147
|
-
const pending = countByTenant('clients', { fresh: true })
|
|
148
|
-
await vi.advanceTimersByTimeAsync(6_000)
|
|
149
|
-
await expect(pending).resolves.toBe(0)
|
|
150
|
-
vi.useRealTimers()
|
|
151
|
-
})
|
|
152
|
-
})
|
package/src/data/count.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { getSupabaseClientOptional } from './supabase'
|
|
2
|
-
import { getActiveTenantId } from '../tenant'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Tenant-scoped row counter — the single cheap `count(*)` helper the access
|
|
6
|
-
* engine uses to resolve quantity limits. Uses Supabase's `head:true` count so
|
|
7
|
-
* no rows are transferred. Results are cached briefly per tenant+table+filter to
|
|
8
|
-
* survive the burst of reads a page does (nav badge + gate + list), and can be
|
|
9
|
-
* force-invalidated by creates via {@link invalidateCount}.
|
|
10
|
-
*
|
|
11
|
-
* MOCK MODE NEVER BLOCKS: with no global Supabase client the count is `0`, so
|
|
12
|
-
* limits always read as "well under cap" — offline/demo apps stay fully usable.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export interface CountByTenantOptions {
|
|
16
|
-
/** Optional `kind` column filter for multi-kind tables. */
|
|
17
|
-
kind?: string
|
|
18
|
-
/** `'month'` counts rows created since the 1st of the current month; `'total'` (default) counts all. */
|
|
19
|
-
period?: 'month' | 'total'
|
|
20
|
-
/** Tenant override; defaults to the active tenant (getActiveTenantId). */
|
|
21
|
-
tenantId?: string
|
|
22
|
-
/** Skip the short-lived cache and hit the DB (used by fresh guard checks). */
|
|
23
|
-
fresh?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const TTL_MS = 15_000
|
|
27
|
-
|
|
28
|
-
interface CacheEntry {
|
|
29
|
-
value: number
|
|
30
|
-
expires: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const cache = new Map<string, CacheEntry>()
|
|
34
|
-
|
|
35
|
-
function cacheKey(table: string, tenantId: string, kind: string | undefined, period: string): string {
|
|
36
|
-
return `${tenantId}::${table}::${kind ?? ''}::${period}`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function startOfMonthISO(): string {
|
|
40
|
-
const now = new Date()
|
|
41
|
-
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* How long a usage count may take before we give up on it. Failing OPEN on an
|
|
46
|
-
* error was always the policy (see below); a request that never settles is the
|
|
47
|
-
* same failure with worse symptoms — a caller awaiting this (the create guards)
|
|
48
|
-
* hangs forever, which reads to the user as a dead button. Observed on a cold
|
|
49
|
-
* boot, where the query can be issued before the auth session is ready.
|
|
50
|
-
*/
|
|
51
|
-
const COUNT_TIMEOUT_MS = 5_000
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Count rows in `table` for the given (or active) tenant.
|
|
55
|
-
* Returns 0 when no Supabase client is registered (mock mode).
|
|
56
|
-
*/
|
|
57
|
-
export async function countByTenant(table: string, options: CountByTenantOptions = {}): Promise<number> {
|
|
58
|
-
const tenantId = options.tenantId ?? getActiveTenantId()
|
|
59
|
-
if (!tenantId) return 0
|
|
60
|
-
|
|
61
|
-
const period = options.period ?? 'total'
|
|
62
|
-
const key = cacheKey(table, tenantId, options.kind, period)
|
|
63
|
-
|
|
64
|
-
if (!options.fresh) {
|
|
65
|
-
const hit = cache.get(key)
|
|
66
|
-
if (hit && hit.expires > Date.now()) return hit.value
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const client = getSupabaseClientOptional() as {
|
|
70
|
-
from: (t: string) => {
|
|
71
|
-
select: (columns: string, opts: { count: 'exact'; head: true }) => {
|
|
72
|
-
eq: (col: string, val: unknown) => unknown
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} | null
|
|
76
|
-
|
|
77
|
-
// No client → mock/offline mode: never block.
|
|
78
|
-
if (!client) return 0
|
|
79
|
-
|
|
80
|
-
let query = client.from(table).select('*', { count: 'exact', head: true }).eq('tenant_id', tenantId) as {
|
|
81
|
-
eq: (col: string, val: unknown) => unknown
|
|
82
|
-
gte: (col: string, val: unknown) => unknown
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (options.kind) {
|
|
86
|
-
query = query.eq('kind', options.kind) as typeof query
|
|
87
|
-
}
|
|
88
|
-
if (period === 'month') {
|
|
89
|
-
query = query.gte('created_at', startOfMonthISO()) as typeof query
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// On error OR timeout, fail OPEN (return 0) — a counting failure must never
|
|
93
|
-
// wall a tenant out of their own product, and must never hang the UI awaiting
|
|
94
|
-
// it. Enforcement's source of truth is the DB anyway.
|
|
95
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
96
|
-
const timeout = new Promise<{ count: null; error: unknown; timedOut: true }>((resolve) => {
|
|
97
|
-
timer = setTimeout(() => resolve({ count: null, error: 'timeout', timedOut: true }), COUNT_TIMEOUT_MS)
|
|
98
|
-
})
|
|
99
|
-
const settled = await Promise.race([
|
|
100
|
-
query as unknown as Promise<{ count: number | null; error: unknown }>,
|
|
101
|
-
timeout,
|
|
102
|
-
])
|
|
103
|
-
if (timer) clearTimeout(timer)
|
|
104
|
-
|
|
105
|
-
const { count, error } = settled
|
|
106
|
-
const value = error ? 0 : count ?? 0
|
|
107
|
-
|
|
108
|
-
// A timed-out count is not a fact about the tenant — caching it would make one
|
|
109
|
-
// slow request suppress the guard for the whole TTL.
|
|
110
|
-
if (!(settled as { timedOut?: true }).timedOut) {
|
|
111
|
-
cache.set(key, { value, expires: Date.now() + TTL_MS })
|
|
112
|
-
}
|
|
113
|
-
return value
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Drop cached counts. Called by create handlers (via invalidateLimit) so the
|
|
118
|
-
* next read reflects the row they just added.
|
|
119
|
-
*
|
|
120
|
-
* @param tenantOrKey Substring matched against cache keys (a tenant id, a table
|
|
121
|
-
* name, or any fragment). Omit to clear the entire cache.
|
|
122
|
-
*/
|
|
123
|
-
export function invalidateCount(tenantOrKey?: string): void {
|
|
124
|
-
if (!tenantOrKey) {
|
|
125
|
-
cache.clear()
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
for (const key of cache.keys()) {
|
|
129
|
-
if (key.includes(tenantOrKey)) cache.delete(key)
|
|
130
|
-
}
|
|
131
|
-
}
|