@beechcms/api 0.6.0-preview.1 → 0.6.0-preview.2
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/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
- package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
- package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +2 -2
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
- package/src/auth/__fixtures__/static-token-service.ts +22 -22
- package/src/auth/bcrypt-hash-provider.ts +24 -24
- package/src/auth/constants.ts +14 -14
- package/src/auth/generate-refresh-token.test.ts +23 -23
- package/src/auth/hash-provider.test.ts +55 -55
- package/src/auth/jose-token-service.ts +62 -62
- package/src/auth/jwt-claims-passthrough.test.ts +87 -87
- package/src/auth/login.test.ts +100 -100
- package/src/auth/login.ts +78 -78
- package/src/auth/refresh.ts +11 -11
- package/src/auth/token-service.test.ts +90 -90
- package/src/factory.ts +370 -368
- package/src/features/automations/__tests__/action-executors.test.ts +255 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
- package/src/features/automations/action-executors/index.ts +37 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
- package/src/features/automations/action-executors/webhook.executor.ts +67 -67
- package/src/features/automations/automation-runner.ts +85 -85
- package/src/features/automations/automation-runner.utils.ts +47 -47
- package/src/features/automations/automations.handler.ts +197 -197
- package/src/features/automations/automations.schema.ts +177 -177
- package/src/features/automations/context-resolver.ts +152 -152
- package/src/features/automations/cron-runner.ts +140 -140
- package/src/features/automations/cron-runner.utils.ts +44 -44
- package/src/features/automations/filter-translation.ts +46 -46
- package/src/features/automations/index.ts +16 -16
- package/src/features/automations/template-grammar.ts +245 -245
- package/src/features/automations/var-access-resolver.ts +140 -140
- package/src/features/automations/when-evaluator.ts +87 -87
- package/src/features/automations/when-pushdown.ts +57 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
- package/src/features/backrefs/backrefs.handler.ts +168 -168
- package/src/features/backrefs/d1-backref.repository.ts +84 -84
- package/src/features/backrefs/index.ts +5 -5
- package/src/features/content/constants.ts +16 -16
- package/src/features/content/handlers/bulk.handler.ts +185 -185
- package/src/features/content/handlers/create.ts +129 -129
- package/src/features/content/handlers/delete.ts +62 -62
- package/src/features/content/handlers/facets.ts +49 -49
- package/src/features/content/handlers/get.ts +120 -120
- package/src/features/content/handlers/helpers.test.ts +180 -180
- package/src/features/content/handlers/helpers.ts +93 -93
- package/src/features/content/handlers/list.ts +174 -174
- package/src/features/content/handlers/update.ts +174 -174
- package/src/features/content/index.ts +26 -26
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
- package/src/features/dashboard-layout/index.ts +5 -0
- package/src/features/draft/draft.handler.ts +195 -195
- package/src/features/draft/draft.middleware.ts +66 -66
- package/src/features/draft/index.ts +5 -5
- package/src/features/email/email.provider.ts +42 -42
- package/src/features/email/email.service.ts +98 -98
- package/src/features/email/email.types.ts +119 -119
- package/src/features/email/index.ts +33 -33
- package/src/features/email/providers/resend.ts +67 -67
- package/src/features/email/providers/smtp.ts +55 -55
- package/src/features/email/templates/automation-mail.ts +19 -19
- package/src/features/email/templates/password-changed.ts +63 -63
- package/src/features/email/templates/password-reset.ts +68 -68
- package/src/features/email/templates/shell.ts +96 -96
- package/src/features/notifications/index.ts +5 -5
- package/src/features/notifications/notifications.handler.ts +105 -105
- package/src/features/password-reset/index.ts +19 -19
- package/src/features/password-reset/request.ts +92 -92
- package/src/features/password-reset/reset.ts +102 -102
- package/src/features/rotate-field/index.ts +5 -5
- package/src/features/rotate-field/rotate-field.handler.ts +132 -132
- package/src/features/rotate-field/rotate-field.schema.ts +17 -17
- package/src/features/schema/schema.handler.ts +131 -131
- package/src/features/seeds/index.ts +5 -5
- package/src/features/seeds/seeds.handler.test.ts +1199 -632
- package/src/features/seeds/seeds.handler.ts +558 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
- package/src/features/settings/settings.handler.ts +392 -392
- package/src/features/setup/index.ts +288 -204
- package/src/features/stats/index.ts +5 -5
- package/src/features/stats/stats.handler.ts +420 -420
- package/src/features/webhooks/index.ts +63 -63
- package/src/index.ts +65 -65
- package/src/media-utils.ts +82 -82
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -45
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -70
- package/src/middleware/rate-limit.middleware.ts +45 -45
- package/src/middleware/repository.middleware.ts +117 -114
- package/src/middleware/seed-registry.middleware.test.ts +97 -97
- package/src/middleware/seed-registry.middleware.ts +21 -21
- package/src/middleware/storage.middleware.ts +25 -25
- package/src/middleware.ts +45 -45
- package/src/public/access-policy.ts +27 -27
- package/src/public/api-key-middleware.ts +57 -57
- package/src/public/cache-utils.ts +38 -38
- package/src/public/entry-projection.ts +46 -46
- package/src/public/idempotency.ts +23 -23
- package/src/public/index.ts +16 -16
- package/src/public/problem-details.ts +124 -124
- package/src/public/public-add.ts +114 -114
- package/src/public/public-edit.ts +163 -163
- package/src/public/public-errors.ts +19 -19
- package/src/public/public-read.ts +63 -63
- package/src/public/public-routes.ts +88 -88
- package/src/public/query-builder.test.ts +224 -224
- package/src/public/query-builder.ts +156 -156
- package/src/public/rate-limit-middleware.ts +34 -34
- package/src/public/read-list.ts +54 -54
- package/src/public/read-single.ts +48 -48
- package/src/public/response-builder.ts +30 -30
- package/src/public/sanitize.ts +69 -69
- package/src/public/slug-utils.ts +18 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
- package/src/rate-limit/no-op-rate-limiter.ts +11 -11
- package/src/search-utils.test.ts +211 -211
- package/src/search-utils.ts +213 -213
- package/src/search.ts +65 -65
- package/src/shared/apply-policies.test.ts +81 -81
- package/src/shared/apply-policies.ts +67 -67
- package/src/shared/automations.repository.d1.ts +150 -150
- package/src/shared/background-notification-service.test.ts +62 -62
- package/src/shared/background-notification-service.ts +52 -52
- package/src/shared/base.repository.d1.ts +32 -32
- package/src/shared/content-utils.test.ts +165 -165
- package/src/shared/content-utils.ts +86 -86
- package/src/shared/content.repository.d1.test.ts +507 -458
- package/src/shared/content.repository.d1.ts +851 -851
- package/src/shared/d1-activity-log.repository.test.ts +140 -140
- package/src/shared/d1-activity-log.repository.ts +105 -105
- package/src/shared/d1-activity-logger.test.ts +86 -86
- package/src/shared/d1-activity-logger.ts +67 -67
- package/src/shared/d1-analytics.repository.test.ts +78 -78
- package/src/shared/d1-analytics.repository.ts +85 -85
- package/src/shared/d1-content-scan.repository.test.ts +80 -80
- package/src/shared/d1-content-scan.repository.ts +37 -37
- package/src/shared/d1-notification.repository.test.ts +128 -128
- package/src/shared/d1-notification.repository.ts +118 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
- package/src/shared/d1-password-reset-token.repository.ts +56 -56
- package/src/shared/d1-search.repository.test.ts +87 -87
- package/src/shared/d1-search.repository.ts +88 -88
- package/src/shared/d1-session.repository.test.ts +125 -125
- package/src/shared/d1-session.repository.ts +102 -102
- package/src/shared/d1-setup-checklist.repository.ts +36 -36
- package/src/shared/d1-user.repository.test.ts +151 -151
- package/src/shared/d1-user.repository.ts +119 -119
- package/src/shared/d1-widget.repository.test.ts +267 -221
- package/src/shared/d1-widget.repository.ts +385 -341
- package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
- package/src/shared/dashboard-layout.repository.d1.ts +58 -0
- package/src/shared/demo-data-sql.ts +216 -54
- package/src/shared/demo-data.repository.d1.ts +20 -20
- package/src/shared/execution-context-scheduler.ts +13 -13
- package/src/shared/fixed-clock.ts +25 -25
- package/src/shared/fts-sync.ts +8 -8
- package/src/shared/idempotency.repository.d1.test.ts +83 -83
- package/src/shared/idempotency.repository.d1.ts +60 -60
- package/src/shared/in-memory-activity-logger.ts +19 -19
- package/src/shared/in-memory-notification-service.ts +19 -19
- package/src/shared/in-memory-seed.repository.ts +58 -51
- package/src/shared/media.repository.d1.test.ts +107 -107
- package/src/shared/media.repository.d1.ts +68 -68
- package/src/shared/qstash-notification-service.test.ts +67 -67
- package/src/shared/qstash-notification-service.ts +55 -55
- package/src/shared/query-utils.ts +141 -141
- package/src/shared/request-utils.ts +26 -26
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -64
- package/src/shared/seed-layout.repository.d1.test.ts +114 -114
- package/src/shared/seed-layout.repository.d1.ts +62 -62
- package/src/shared/seed-registry-cache.test.ts +68 -68
- package/src/shared/seed-registry-cache.ts +49 -49
- package/src/shared/seed.repository.d1.test.ts +143 -143
- package/src/shared/seed.repository.d1.ts +98 -98
- package/src/shared/sequential-id-generator.ts +33 -33
- package/src/shared/site-settings.repository.d1.ts +53 -53
- package/src/shared/storage/factory.ts +41 -41
- package/src/shared/storage/s3-bucket.ts +195 -195
- package/src/shared/storage-utils.ts +40 -40
- package/src/shared/system-stats.repository.d1.test.ts +58 -58
- package/src/shared/system-stats.repository.d1.ts +48 -48
- package/src/types.ts +90 -87
- package/src/upload.ts +198 -198
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -212
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
|
@@ -1,383 +1,383 @@
|
|
|
1
|
-
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
-
// See LICENSE in the repository root for license terms.
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
6
|
-
import { executeSetVariable } from '../action-executors/set-variable.executor'
|
|
7
|
-
import { resolveAutomationContext } from '../context-resolver'
|
|
8
|
-
import { resolveVarAccess } from '../var-access-resolver'
|
|
9
|
-
import { parseTemplateKey } from '../template-grammar'
|
|
10
|
-
import type { ContentRepository, Seed } from '@beechcms/core'
|
|
11
|
-
|
|
12
|
-
const MOCK_SEED: Seed = {
|
|
13
|
-
slug: 'clienti',
|
|
14
|
-
label: 'Clienti',
|
|
15
|
-
branches: [
|
|
16
|
-
{ alias: 'name', label: 'Name', type: 'text', id: 'br_01' },
|
|
17
|
-
{ alias: 'email', label: 'Email', type: 'text', id: 'br_02' },
|
|
18
|
-
{ alias: 'total', label: 'Total', type: 'number', id: 'br_03' },
|
|
19
|
-
],
|
|
20
|
-
} as unknown as Seed
|
|
21
|
-
|
|
22
|
-
async function makeCtx(overrides: Partial<{
|
|
23
|
-
entry: Record<string, unknown>
|
|
24
|
-
variables: Record<string, unknown>
|
|
25
|
-
repository: ContentRepository
|
|
26
|
-
getSeed: (slug: string) => Seed | null
|
|
27
|
-
seed: Seed
|
|
28
|
-
}> = {}) {
|
|
29
|
-
const entry = overrides.entry ?? { id: 'entry-1', customer_id: 'cust-42' }
|
|
30
|
-
const variables = overrides.variables ?? {}
|
|
31
|
-
const baseContext = await resolveAutomationContext({} as any, entry, [entry])
|
|
32
|
-
const context = {
|
|
33
|
-
triggerEntry: baseContext.triggerEntry,
|
|
34
|
-
lookup(parsed: any, onMissing?: any) {
|
|
35
|
-
if (parsed.kind === 'simple') {
|
|
36
|
-
const varVal = (variables as any)[parsed.path]
|
|
37
|
-
if (varVal !== undefined) return varVal
|
|
38
|
-
if (parsed.path.includes('.')) {
|
|
39
|
-
const [first, ...rest] = parsed.path.split('.')
|
|
40
|
-
const varRoot = (variables as any)[first]
|
|
41
|
-
if (varRoot !== undefined) {
|
|
42
|
-
let cur: any = varRoot
|
|
43
|
-
for (const k of rest) cur = cur?.[k]
|
|
44
|
-
if (cur !== undefined) return cur
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} else if (parsed.kind === 'var_access') {
|
|
48
|
-
return resolveVarAccess(parsed, variables, onMissing)
|
|
49
|
-
}
|
|
50
|
-
return baseContext.lookup(parsed, onMissing)
|
|
51
|
-
},
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
entry,
|
|
55
|
-
variables,
|
|
56
|
-
repository: overrides.repository ?? {
|
|
57
|
-
findMany: vi.fn().mockResolvedValue({ items: [] }),
|
|
58
|
-
} as unknown as ContentRepository,
|
|
59
|
-
getSeed: overrides.getSeed ?? vi.fn().mockReturnValue(MOCK_SEED),
|
|
60
|
-
seed: overrides.seed ?? MOCK_SEED,
|
|
61
|
-
context,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
describe('fixed_id mode', () => {
|
|
66
|
-
it('1 — fixed_id set, record exists → variables[name] equals item', async () => {
|
|
67
|
-
const item = { id: 'c_1', name: 'Mario', email: 'mario@x.com', total: 99 }
|
|
68
|
-
const findMany = vi.fn().mockResolvedValue({ items: [item] })
|
|
69
|
-
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
70
|
-
|
|
71
|
-
await executeSetVariable(
|
|
72
|
-
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: 'c_1' },
|
|
73
|
-
ctx,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
expect(ctx.variables.cliente).toEqual(item)
|
|
77
|
-
const [, opts] = findMany.mock.calls[0]
|
|
78
|
-
expect(opts.filters[0].conditions[0].value).toBe('c_1')
|
|
79
|
-
expect(opts.pagination.limit).toBe(1)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('2 — fixed_id set, record missing → null + console.warn', async () => {
|
|
83
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
84
|
-
const ctx = await makeCtx({
|
|
85
|
-
repository: { findMany: vi.fn().mockResolvedValue({ items: [] }) } as unknown as ContentRepository,
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
await executeSetVariable(
|
|
89
|
-
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: 'ghost' },
|
|
90
|
-
ctx,
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
expect(ctx.variables.cliente).toBeNull()
|
|
94
|
-
expect(warnSpy).toHaveBeenCalled()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('3 — fixed_id + column → { _value: item[column] }', async () => {
|
|
98
|
-
const item = { id: 'c_1', name: 'Mario', email: 'mario@x.com', total: 99 }
|
|
99
|
-
const ctx = await makeCtx({
|
|
100
|
-
repository: { findMany: vi.fn().mockResolvedValue({ items: [item] }) } as unknown as ContentRepository,
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
await executeSetVariable(
|
|
104
|
-
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: 'c_1', column: 'email' },
|
|
105
|
-
ctx,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
expect(ctx.variables.cliente).toEqual({ _value: 'mario@x.com' })
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('4 — fixed_id interpolation from this scope (dot notation)', async () => {
|
|
112
|
-
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
113
|
-
const ctx = await makeCtx({
|
|
114
|
-
entry: { id: 'e1', customer_id: 'cust-77' },
|
|
115
|
-
repository: { findMany } as unknown as ContentRepository,
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
await executeSetVariable(
|
|
119
|
-
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: '{{this.customer_id}}' },
|
|
120
|
-
ctx,
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
const [, opts] = findMany.mock.calls[0]
|
|
124
|
-
expect(opts.filters[0].conditions[0].value).toBe('cust-77')
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
describe('collection mode', () => {
|
|
129
|
-
it('5 — no filters: count, firstone, lastone, sum, pluck correct', async () => {
|
|
130
|
-
const items = [
|
|
131
|
-
{ id: '1', name: 'A', email: 'a@x.com', total: 10, created_at: 1000 },
|
|
132
|
-
{ id: '2', name: 'B', email: 'b@x.com', total: 20, created_at: 2000 },
|
|
133
|
-
{ id: '3', name: 'C', email: 'c@x.com', total: 30, created_at: 500 },
|
|
134
|
-
]
|
|
135
|
-
const ctx = await makeCtx({
|
|
136
|
-
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
await executeSetVariable(
|
|
140
|
-
{ type: 'set_variable', name: 'ordini', seed_slug: 'clienti' },
|
|
141
|
-
ctx,
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
const result = ctx.variables.ordini as any
|
|
145
|
-
expect(result.count).toBe(3)
|
|
146
|
-
expect(result.sum.total).toBe(60)
|
|
147
|
-
expect(result.firstone).toEqual(items[2])
|
|
148
|
-
expect(result.lastone).toEqual(items[1])
|
|
149
|
-
expect(result.pluck.name).toBe('A, B, C')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('6 — collection with filters → findMany receives resolved filter groups', async () => {
|
|
153
|
-
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
154
|
-
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
155
|
-
|
|
156
|
-
await executeSetVariable(
|
|
157
|
-
{
|
|
158
|
-
type: 'set_variable',
|
|
159
|
-
name: 'ordini',
|
|
160
|
-
seed_slug: 'clienti',
|
|
161
|
-
filters: [{ field: 'name', op: 'eq', value: 'Mario' }],
|
|
162
|
-
},
|
|
163
|
-
ctx,
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
const [, opts] = findMany.mock.calls[0]
|
|
167
|
-
expect(opts.filters).toHaveLength(1)
|
|
168
|
-
expect(opts.filters[0].column).toBe('name')
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('7 — collection with column pin: scalar aggregates + count non-null', async () => {
|
|
172
|
-
const items = [
|
|
173
|
-
{ id: '1', total: 10, created_at: 1 },
|
|
174
|
-
{ id: '2', total: null, created_at: 2 },
|
|
175
|
-
{ id: '3', total: 20, created_at: 3 },
|
|
176
|
-
]
|
|
177
|
-
const ctx = await makeCtx({
|
|
178
|
-
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
await executeSetVariable(
|
|
182
|
-
{ type: 'set_variable', name: 'r', seed_slug: 'clienti', column: 'total' },
|
|
183
|
-
ctx,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
const result = ctx.variables.r as any
|
|
187
|
-
expect(result.count).toBe(2)
|
|
188
|
-
expect(result.sum).toBe(30)
|
|
189
|
-
expect(result.avg).toBe(15)
|
|
190
|
-
expect(result.min).toBe(10)
|
|
191
|
-
expect(result.max).toBe(20)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('8 — seed_slug omitted → uses ctx.seed.slug', async () => {
|
|
195
|
-
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
196
|
-
const triggerSeed: Seed = { ...MOCK_SEED, slug: 'ordini' }
|
|
197
|
-
const ctx = await makeCtx({
|
|
198
|
-
repository: { findMany } as unknown as ContentRepository,
|
|
199
|
-
getSeed: vi.fn().mockReturnValue(triggerSeed),
|
|
200
|
-
seed: triggerSeed,
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
await executeSetVariable(
|
|
204
|
-
{ type: 'set_variable', name: 'result' },
|
|
205
|
-
ctx,
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
const [seed] = findMany.mock.calls[0]
|
|
209
|
-
expect(seed.slug).toBe('ordini')
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('9 — seed not found → null + warn', async () => {
|
|
213
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
214
|
-
const ctx = await makeCtx({ getSeed: vi.fn().mockReturnValue(null) })
|
|
215
|
-
|
|
216
|
-
await executeSetVariable(
|
|
217
|
-
{ type: 'set_variable', name: 'x', seed_slug: 'ghost' },
|
|
218
|
-
ctx,
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
expect(ctx.variables.x).toBeNull()
|
|
222
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ghost'))
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('10 — filter value interpolated from a previously set variable', async () => {
|
|
226
|
-
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
227
|
-
const ctx = await makeCtx({
|
|
228
|
-
variables: { cliente: { id: 'cust-123' } } as Record<string, unknown>,
|
|
229
|
-
repository: { findMany } as unknown as ContentRepository,
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
await executeSetVariable(
|
|
233
|
-
{
|
|
234
|
-
type: 'set_variable',
|
|
235
|
-
name: 'ordini',
|
|
236
|
-
seed_slug: 'clienti',
|
|
237
|
-
filters: [{ field: 'customer_id', op: 'eq', value: '{{cliente.id}}' }],
|
|
238
|
-
},
|
|
239
|
-
ctx,
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
const [, opts] = findMany.mock.calls[0]
|
|
243
|
-
expect(opts.filters[0].conditions[0].value).toBe('cust-123')
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it('12 — date branch: sum/avg/min/max computed from ISO strings', async () => {
|
|
247
|
-
const dateSeed: Seed = {
|
|
248
|
-
...MOCK_SEED,
|
|
249
|
-
branches: [
|
|
250
|
-
{ alias: 'publishedAt', label: 'Published', type: 'date', id: 'br_d1' },
|
|
251
|
-
],
|
|
252
|
-
} as unknown as Seed
|
|
253
|
-
const items = [
|
|
254
|
-
{ id: '1', publishedAt: '2025-01-10T00:00:00.000Z', created_at: 1 },
|
|
255
|
-
{ id: '2', publishedAt: '2025-03-05T00:00:00.000Z', created_at: 2 },
|
|
256
|
-
{ id: '3', publishedAt: '2025-06-01T00:00:00.000Z', created_at: 3 },
|
|
257
|
-
]
|
|
258
|
-
const ctx = await makeCtx({
|
|
259
|
-
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
260
|
-
getSeed: vi.fn().mockReturnValue(dateSeed),
|
|
261
|
-
seed: dateSeed,
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
await executeSetVariable({ type: 'set_variable', name: 'articoli', seed_slug: 'clienti' }, ctx)
|
|
265
|
-
|
|
266
|
-
const result = ctx.variables.articoli as any
|
|
267
|
-
expect(result.sum.publishedAt).toBeGreaterThan(0)
|
|
268
|
-
expect(result.avg.publishedAt).toBeGreaterThan(0)
|
|
269
|
-
expect(result.min.publishedAt).toBeLessThan(result.max.publishedAt)
|
|
270
|
-
const minDate = new Date('2025-01-10T00:00:00.000Z').getTime() / 1000
|
|
271
|
-
const maxDate = new Date('2025-06-01T00:00:00.000Z').getTime() / 1000
|
|
272
|
-
expect(result.min.publishedAt).toBe(minDate)
|
|
273
|
-
expect(result.max.publishedAt).toBe(maxDate)
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('13 — var_access inline condition on ISO date field', async () => {
|
|
277
|
-
const items = [
|
|
278
|
-
{ id: '1', publishedAt: '2025-01-10T00:00:00.000Z', created_at: 1 },
|
|
279
|
-
{ id: '2', publishedAt: '2025-03-05T00:00:00.000Z', created_at: 2 },
|
|
280
|
-
{ id: '3', publishedAt: '2026-06-01T00:00:00.000Z', created_at: 3 },
|
|
281
|
-
]
|
|
282
|
-
const ctx = await makeCtx({
|
|
283
|
-
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
284
|
-
})
|
|
285
|
-
await executeSetVariable({ type: 'set_variable', name: 'arts', seed_slug: 'clienti' }, ctx)
|
|
286
|
-
|
|
287
|
-
const parsed = parseTemplateKey('arts.(publishedAt>1740000000).count')
|
|
288
|
-
expect(parsed?.kind).toBe('var_access')
|
|
289
|
-
const result = ctx.context.lookup(parsed!)
|
|
290
|
-
// 1740000000 = ~2025-02-20; art-0002 (Mar 2025) and art-0003 (Jun 2026) qualify
|
|
291
|
-
expect(result).toBe(2)
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
it('11 — _items is non-enumerable (not in JSON.stringify)', async () => {
|
|
295
|
-
const items = [{ id: '1', name: 'A', email: 'a@x.com', total: 5, created_at: 1 }]
|
|
296
|
-
const ctx = await makeCtx({
|
|
297
|
-
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
await executeSetVariable(
|
|
301
|
-
{ type: 'set_variable', name: 'ordini', seed_slug: 'clienti' },
|
|
302
|
-
ctx,
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
const json = JSON.stringify(ctx.variables)
|
|
306
|
-
expect(json).not.toContain('_items')
|
|
307
|
-
const result = ctx.variables.ordini as any
|
|
308
|
-
expect(result._items).toEqual(items)
|
|
309
|
-
})
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
// ── Sprint 07: backward-compatibility with legacy load_type field ──────────────
|
|
313
|
-
|
|
314
|
-
describe('legacy load_type backward compatibility', () => {
|
|
315
|
-
it('L1 — load_type: fruit + single id-eq filter → upgraded to fixed_id path', async () => {
|
|
316
|
-
const item = { id: 'c_1', name: 'Mario', email: 'mario@x.com', total: 99 }
|
|
317
|
-
const findMany = vi.fn().mockResolvedValue({ items: [item] })
|
|
318
|
-
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
319
|
-
|
|
320
|
-
await executeSetVariable(
|
|
321
|
-
{
|
|
322
|
-
type: 'set_variable',
|
|
323
|
-
name: 'cliente',
|
|
324
|
-
seed_slug: 'clienti',
|
|
325
|
-
load_type: 'fruit',
|
|
326
|
-
filters: [{ field: 'id', op: 'eq', value: 'c_1' }],
|
|
327
|
-
} as any,
|
|
328
|
-
ctx,
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
// Should use fixed_id path: pagination.limit = 1, filter on id column
|
|
332
|
-
const [, opts] = findMany.mock.calls[0]
|
|
333
|
-
expect(opts.filters[0].column).toBe('id')
|
|
334
|
-
expect(opts.filters[0].conditions[0].value).toBe('c_1')
|
|
335
|
-
expect(opts.pagination.limit).toBe(1)
|
|
336
|
-
expect(ctx.variables.cliente).toEqual(item)
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('L2 — load_type: fruit + non-id filter → collection path (degraded)', async () => {
|
|
340
|
-
const items = [{ id: '1', name: 'Mario', email: 'mario@x.com', total: 10, created_at: 1 }]
|
|
341
|
-
const findMany = vi.fn().mockResolvedValue({ items })
|
|
342
|
-
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
343
|
-
|
|
344
|
-
await executeSetVariable(
|
|
345
|
-
{
|
|
346
|
-
type: 'set_variable',
|
|
347
|
-
name: 'clienti',
|
|
348
|
-
seed_slug: 'clienti',
|
|
349
|
-
load_type: 'fruit',
|
|
350
|
-
filters: [{ field: 'name', op: 'eq', value: 'Mario' }],
|
|
351
|
-
} as any,
|
|
352
|
-
ctx,
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
// Non-id filter: falls through to collection path
|
|
356
|
-
const result = ctx.variables.clienti as any
|
|
357
|
-
expect(result.count).toBe(1)
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
it('L3 — load_type: branch → collection path', async () => {
|
|
361
|
-
const items = [
|
|
362
|
-
{ id: '1', name: 'A', email: 'a@x.com', total: 10, created_at: 1 },
|
|
363
|
-
{ id: '2', name: 'B', email: 'b@x.com', total: 20, created_at: 2 },
|
|
364
|
-
]
|
|
365
|
-
const findMany = vi.fn().mockResolvedValue({ items })
|
|
366
|
-
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
367
|
-
|
|
368
|
-
await executeSetVariable(
|
|
369
|
-
{
|
|
370
|
-
type: 'set_variable',
|
|
371
|
-
name: 'ordini',
|
|
372
|
-
seed_slug: 'clienti',
|
|
373
|
-
load_type: 'branch',
|
|
374
|
-
} as any,
|
|
375
|
-
ctx,
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
const result = ctx.variables.ordini as any
|
|
379
|
-
expect(result.count).toBe(2)
|
|
380
|
-
expect(result.sum).toBeDefined()
|
|
381
|
-
expect(result.pluck).toBeDefined()
|
|
382
|
-
})
|
|
383
|
-
})
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
6
|
+
import { executeSetVariable } from '../action-executors/set-variable.executor'
|
|
7
|
+
import { resolveAutomationContext } from '../context-resolver'
|
|
8
|
+
import { resolveVarAccess } from '../var-access-resolver'
|
|
9
|
+
import { parseTemplateKey } from '../template-grammar'
|
|
10
|
+
import type { ContentRepository, Seed } from '@beechcms/core'
|
|
11
|
+
|
|
12
|
+
const MOCK_SEED: Seed = {
|
|
13
|
+
slug: 'clienti',
|
|
14
|
+
label: 'Clienti',
|
|
15
|
+
branches: [
|
|
16
|
+
{ alias: 'name', label: 'Name', type: 'text', id: 'br_01' },
|
|
17
|
+
{ alias: 'email', label: 'Email', type: 'text', id: 'br_02' },
|
|
18
|
+
{ alias: 'total', label: 'Total', type: 'number', id: 'br_03' },
|
|
19
|
+
],
|
|
20
|
+
} as unknown as Seed
|
|
21
|
+
|
|
22
|
+
async function makeCtx(overrides: Partial<{
|
|
23
|
+
entry: Record<string, unknown>
|
|
24
|
+
variables: Record<string, unknown>
|
|
25
|
+
repository: ContentRepository
|
|
26
|
+
getSeed: (slug: string) => Seed | null
|
|
27
|
+
seed: Seed
|
|
28
|
+
}> = {}) {
|
|
29
|
+
const entry = overrides.entry ?? { id: 'entry-1', customer_id: 'cust-42' }
|
|
30
|
+
const variables = overrides.variables ?? {}
|
|
31
|
+
const baseContext = await resolveAutomationContext({} as any, entry, [entry])
|
|
32
|
+
const context = {
|
|
33
|
+
triggerEntry: baseContext.triggerEntry,
|
|
34
|
+
lookup(parsed: any, onMissing?: any) {
|
|
35
|
+
if (parsed.kind === 'simple') {
|
|
36
|
+
const varVal = (variables as any)[parsed.path]
|
|
37
|
+
if (varVal !== undefined) return varVal
|
|
38
|
+
if (parsed.path.includes('.')) {
|
|
39
|
+
const [first, ...rest] = parsed.path.split('.')
|
|
40
|
+
const varRoot = (variables as any)[first]
|
|
41
|
+
if (varRoot !== undefined) {
|
|
42
|
+
let cur: any = varRoot
|
|
43
|
+
for (const k of rest) cur = cur?.[k]
|
|
44
|
+
if (cur !== undefined) return cur
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (parsed.kind === 'var_access') {
|
|
48
|
+
return resolveVarAccess(parsed, variables, onMissing)
|
|
49
|
+
}
|
|
50
|
+
return baseContext.lookup(parsed, onMissing)
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
entry,
|
|
55
|
+
variables,
|
|
56
|
+
repository: overrides.repository ?? {
|
|
57
|
+
findMany: vi.fn().mockResolvedValue({ items: [] }),
|
|
58
|
+
} as unknown as ContentRepository,
|
|
59
|
+
getSeed: overrides.getSeed ?? vi.fn().mockReturnValue(MOCK_SEED),
|
|
60
|
+
seed: overrides.seed ?? MOCK_SEED,
|
|
61
|
+
context,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('fixed_id mode', () => {
|
|
66
|
+
it('1 — fixed_id set, record exists → variables[name] equals item', async () => {
|
|
67
|
+
const item = { id: 'c_1', name: 'Mario', email: 'mario@x.com', total: 99 }
|
|
68
|
+
const findMany = vi.fn().mockResolvedValue({ items: [item] })
|
|
69
|
+
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
70
|
+
|
|
71
|
+
await executeSetVariable(
|
|
72
|
+
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: 'c_1' },
|
|
73
|
+
ctx,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(ctx.variables.cliente).toEqual(item)
|
|
77
|
+
const [, opts] = findMany.mock.calls[0]
|
|
78
|
+
expect(opts.filters[0].conditions[0].value).toBe('c_1')
|
|
79
|
+
expect(opts.pagination.limit).toBe(1)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('2 — fixed_id set, record missing → null + console.warn', async () => {
|
|
83
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
84
|
+
const ctx = await makeCtx({
|
|
85
|
+
repository: { findMany: vi.fn().mockResolvedValue({ items: [] }) } as unknown as ContentRepository,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await executeSetVariable(
|
|
89
|
+
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: 'ghost' },
|
|
90
|
+
ctx,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
expect(ctx.variables.cliente).toBeNull()
|
|
94
|
+
expect(warnSpy).toHaveBeenCalled()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('3 — fixed_id + column → { _value: item[column] }', async () => {
|
|
98
|
+
const item = { id: 'c_1', name: 'Mario', email: 'mario@x.com', total: 99 }
|
|
99
|
+
const ctx = await makeCtx({
|
|
100
|
+
repository: { findMany: vi.fn().mockResolvedValue({ items: [item] }) } as unknown as ContentRepository,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await executeSetVariable(
|
|
104
|
+
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: 'c_1', column: 'email' },
|
|
105
|
+
ctx,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(ctx.variables.cliente).toEqual({ _value: 'mario@x.com' })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('4 — fixed_id interpolation from this scope (dot notation)', async () => {
|
|
112
|
+
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
113
|
+
const ctx = await makeCtx({
|
|
114
|
+
entry: { id: 'e1', customer_id: 'cust-77' },
|
|
115
|
+
repository: { findMany } as unknown as ContentRepository,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await executeSetVariable(
|
|
119
|
+
{ type: 'set_variable', name: 'cliente', seed_slug: 'clienti', fixed_id: '{{this.customer_id}}' },
|
|
120
|
+
ctx,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const [, opts] = findMany.mock.calls[0]
|
|
124
|
+
expect(opts.filters[0].conditions[0].value).toBe('cust-77')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('collection mode', () => {
|
|
129
|
+
it('5 — no filters: count, firstone, lastone, sum, pluck correct', async () => {
|
|
130
|
+
const items = [
|
|
131
|
+
{ id: '1', name: 'A', email: 'a@x.com', total: 10, created_at: 1000 },
|
|
132
|
+
{ id: '2', name: 'B', email: 'b@x.com', total: 20, created_at: 2000 },
|
|
133
|
+
{ id: '3', name: 'C', email: 'c@x.com', total: 30, created_at: 500 },
|
|
134
|
+
]
|
|
135
|
+
const ctx = await makeCtx({
|
|
136
|
+
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await executeSetVariable(
|
|
140
|
+
{ type: 'set_variable', name: 'ordini', seed_slug: 'clienti' },
|
|
141
|
+
ctx,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const result = ctx.variables.ordini as any
|
|
145
|
+
expect(result.count).toBe(3)
|
|
146
|
+
expect(result.sum.total).toBe(60)
|
|
147
|
+
expect(result.firstone).toEqual(items[2])
|
|
148
|
+
expect(result.lastone).toEqual(items[1])
|
|
149
|
+
expect(result.pluck.name).toBe('A, B, C')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('6 — collection with filters → findMany receives resolved filter groups', async () => {
|
|
153
|
+
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
154
|
+
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
155
|
+
|
|
156
|
+
await executeSetVariable(
|
|
157
|
+
{
|
|
158
|
+
type: 'set_variable',
|
|
159
|
+
name: 'ordini',
|
|
160
|
+
seed_slug: 'clienti',
|
|
161
|
+
filters: [{ field: 'name', op: 'eq', value: 'Mario' }],
|
|
162
|
+
},
|
|
163
|
+
ctx,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const [, opts] = findMany.mock.calls[0]
|
|
167
|
+
expect(opts.filters).toHaveLength(1)
|
|
168
|
+
expect(opts.filters[0].column).toBe('name')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('7 — collection with column pin: scalar aggregates + count non-null', async () => {
|
|
172
|
+
const items = [
|
|
173
|
+
{ id: '1', total: 10, created_at: 1 },
|
|
174
|
+
{ id: '2', total: null, created_at: 2 },
|
|
175
|
+
{ id: '3', total: 20, created_at: 3 },
|
|
176
|
+
]
|
|
177
|
+
const ctx = await makeCtx({
|
|
178
|
+
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
await executeSetVariable(
|
|
182
|
+
{ type: 'set_variable', name: 'r', seed_slug: 'clienti', column: 'total' },
|
|
183
|
+
ctx,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const result = ctx.variables.r as any
|
|
187
|
+
expect(result.count).toBe(2)
|
|
188
|
+
expect(result.sum).toBe(30)
|
|
189
|
+
expect(result.avg).toBe(15)
|
|
190
|
+
expect(result.min).toBe(10)
|
|
191
|
+
expect(result.max).toBe(20)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('8 — seed_slug omitted → uses ctx.seed.slug', async () => {
|
|
195
|
+
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
196
|
+
const triggerSeed: Seed = { ...MOCK_SEED, slug: 'ordini' }
|
|
197
|
+
const ctx = await makeCtx({
|
|
198
|
+
repository: { findMany } as unknown as ContentRepository,
|
|
199
|
+
getSeed: vi.fn().mockReturnValue(triggerSeed),
|
|
200
|
+
seed: triggerSeed,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await executeSetVariable(
|
|
204
|
+
{ type: 'set_variable', name: 'result' },
|
|
205
|
+
ctx,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const [seed] = findMany.mock.calls[0]
|
|
209
|
+
expect(seed.slug).toBe('ordini')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('9 — seed not found → null + warn', async () => {
|
|
213
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
214
|
+
const ctx = await makeCtx({ getSeed: vi.fn().mockReturnValue(null) })
|
|
215
|
+
|
|
216
|
+
await executeSetVariable(
|
|
217
|
+
{ type: 'set_variable', name: 'x', seed_slug: 'ghost' },
|
|
218
|
+
ctx,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
expect(ctx.variables.x).toBeNull()
|
|
222
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ghost'))
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('10 — filter value interpolated from a previously set variable', async () => {
|
|
226
|
+
const findMany = vi.fn().mockResolvedValue({ items: [] })
|
|
227
|
+
const ctx = await makeCtx({
|
|
228
|
+
variables: { cliente: { id: 'cust-123' } } as Record<string, unknown>,
|
|
229
|
+
repository: { findMany } as unknown as ContentRepository,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
await executeSetVariable(
|
|
233
|
+
{
|
|
234
|
+
type: 'set_variable',
|
|
235
|
+
name: 'ordini',
|
|
236
|
+
seed_slug: 'clienti',
|
|
237
|
+
filters: [{ field: 'customer_id', op: 'eq', value: '{{cliente.id}}' }],
|
|
238
|
+
},
|
|
239
|
+
ctx,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const [, opts] = findMany.mock.calls[0]
|
|
243
|
+
expect(opts.filters[0].conditions[0].value).toBe('cust-123')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('12 — date branch: sum/avg/min/max computed from ISO strings', async () => {
|
|
247
|
+
const dateSeed: Seed = {
|
|
248
|
+
...MOCK_SEED,
|
|
249
|
+
branches: [
|
|
250
|
+
{ alias: 'publishedAt', label: 'Published', type: 'date', id: 'br_d1' },
|
|
251
|
+
],
|
|
252
|
+
} as unknown as Seed
|
|
253
|
+
const items = [
|
|
254
|
+
{ id: '1', publishedAt: '2025-01-10T00:00:00.000Z', created_at: 1 },
|
|
255
|
+
{ id: '2', publishedAt: '2025-03-05T00:00:00.000Z', created_at: 2 },
|
|
256
|
+
{ id: '3', publishedAt: '2025-06-01T00:00:00.000Z', created_at: 3 },
|
|
257
|
+
]
|
|
258
|
+
const ctx = await makeCtx({
|
|
259
|
+
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
260
|
+
getSeed: vi.fn().mockReturnValue(dateSeed),
|
|
261
|
+
seed: dateSeed,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
await executeSetVariable({ type: 'set_variable', name: 'articoli', seed_slug: 'clienti' }, ctx)
|
|
265
|
+
|
|
266
|
+
const result = ctx.variables.articoli as any
|
|
267
|
+
expect(result.sum.publishedAt).toBeGreaterThan(0)
|
|
268
|
+
expect(result.avg.publishedAt).toBeGreaterThan(0)
|
|
269
|
+
expect(result.min.publishedAt).toBeLessThan(result.max.publishedAt)
|
|
270
|
+
const minDate = new Date('2025-01-10T00:00:00.000Z').getTime() / 1000
|
|
271
|
+
const maxDate = new Date('2025-06-01T00:00:00.000Z').getTime() / 1000
|
|
272
|
+
expect(result.min.publishedAt).toBe(minDate)
|
|
273
|
+
expect(result.max.publishedAt).toBe(maxDate)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('13 — var_access inline condition on ISO date field', async () => {
|
|
277
|
+
const items = [
|
|
278
|
+
{ id: '1', publishedAt: '2025-01-10T00:00:00.000Z', created_at: 1 },
|
|
279
|
+
{ id: '2', publishedAt: '2025-03-05T00:00:00.000Z', created_at: 2 },
|
|
280
|
+
{ id: '3', publishedAt: '2026-06-01T00:00:00.000Z', created_at: 3 },
|
|
281
|
+
]
|
|
282
|
+
const ctx = await makeCtx({
|
|
283
|
+
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
284
|
+
})
|
|
285
|
+
await executeSetVariable({ type: 'set_variable', name: 'arts', seed_slug: 'clienti' }, ctx)
|
|
286
|
+
|
|
287
|
+
const parsed = parseTemplateKey('arts.(publishedAt>1740000000).count')
|
|
288
|
+
expect(parsed?.kind).toBe('var_access')
|
|
289
|
+
const result = ctx.context.lookup(parsed!)
|
|
290
|
+
// 1740000000 = ~2025-02-20; art-0002 (Mar 2025) and art-0003 (Jun 2026) qualify
|
|
291
|
+
expect(result).toBe(2)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('11 — _items is non-enumerable (not in JSON.stringify)', async () => {
|
|
295
|
+
const items = [{ id: '1', name: 'A', email: 'a@x.com', total: 5, created_at: 1 }]
|
|
296
|
+
const ctx = await makeCtx({
|
|
297
|
+
repository: { findMany: vi.fn().mockResolvedValue({ items }) } as unknown as ContentRepository,
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
await executeSetVariable(
|
|
301
|
+
{ type: 'set_variable', name: 'ordini', seed_slug: 'clienti' },
|
|
302
|
+
ctx,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const json = JSON.stringify(ctx.variables)
|
|
306
|
+
expect(json).not.toContain('_items')
|
|
307
|
+
const result = ctx.variables.ordini as any
|
|
308
|
+
expect(result._items).toEqual(items)
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// ── Sprint 07: backward-compatibility with legacy load_type field ──────────────
|
|
313
|
+
|
|
314
|
+
describe('legacy load_type backward compatibility', () => {
|
|
315
|
+
it('L1 — load_type: fruit + single id-eq filter → upgraded to fixed_id path', async () => {
|
|
316
|
+
const item = { id: 'c_1', name: 'Mario', email: 'mario@x.com', total: 99 }
|
|
317
|
+
const findMany = vi.fn().mockResolvedValue({ items: [item] })
|
|
318
|
+
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
319
|
+
|
|
320
|
+
await executeSetVariable(
|
|
321
|
+
{
|
|
322
|
+
type: 'set_variable',
|
|
323
|
+
name: 'cliente',
|
|
324
|
+
seed_slug: 'clienti',
|
|
325
|
+
load_type: 'fruit',
|
|
326
|
+
filters: [{ field: 'id', op: 'eq', value: 'c_1' }],
|
|
327
|
+
} as any,
|
|
328
|
+
ctx,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
// Should use fixed_id path: pagination.limit = 1, filter on id column
|
|
332
|
+
const [, opts] = findMany.mock.calls[0]
|
|
333
|
+
expect(opts.filters[0].column).toBe('id')
|
|
334
|
+
expect(opts.filters[0].conditions[0].value).toBe('c_1')
|
|
335
|
+
expect(opts.pagination.limit).toBe(1)
|
|
336
|
+
expect(ctx.variables.cliente).toEqual(item)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('L2 — load_type: fruit + non-id filter → collection path (degraded)', async () => {
|
|
340
|
+
const items = [{ id: '1', name: 'Mario', email: 'mario@x.com', total: 10, created_at: 1 }]
|
|
341
|
+
const findMany = vi.fn().mockResolvedValue({ items })
|
|
342
|
+
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
343
|
+
|
|
344
|
+
await executeSetVariable(
|
|
345
|
+
{
|
|
346
|
+
type: 'set_variable',
|
|
347
|
+
name: 'clienti',
|
|
348
|
+
seed_slug: 'clienti',
|
|
349
|
+
load_type: 'fruit',
|
|
350
|
+
filters: [{ field: 'name', op: 'eq', value: 'Mario' }],
|
|
351
|
+
} as any,
|
|
352
|
+
ctx,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
// Non-id filter: falls through to collection path
|
|
356
|
+
const result = ctx.variables.clienti as any
|
|
357
|
+
expect(result.count).toBe(1)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('L3 — load_type: branch → collection path', async () => {
|
|
361
|
+
const items = [
|
|
362
|
+
{ id: '1', name: 'A', email: 'a@x.com', total: 10, created_at: 1 },
|
|
363
|
+
{ id: '2', name: 'B', email: 'b@x.com', total: 20, created_at: 2 },
|
|
364
|
+
]
|
|
365
|
+
const findMany = vi.fn().mockResolvedValue({ items })
|
|
366
|
+
const ctx = await makeCtx({ repository: { findMany } as unknown as ContentRepository })
|
|
367
|
+
|
|
368
|
+
await executeSetVariable(
|
|
369
|
+
{
|
|
370
|
+
type: 'set_variable',
|
|
371
|
+
name: 'ordini',
|
|
372
|
+
seed_slug: 'clienti',
|
|
373
|
+
load_type: 'branch',
|
|
374
|
+
} as any,
|
|
375
|
+
ctx,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
const result = ctx.variables.ordini as any
|
|
379
|
+
expect(result.count).toBe(2)
|
|
380
|
+
expect(result.sum).toBeDefined()
|
|
381
|
+
expect(result.pluck).toBeDefined()
|
|
382
|
+
})
|
|
383
|
+
})
|