@beechcms/api 0.5.0 → 0.6.0-preview.1
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/Searching.svg +1 -0
- package/assets/dashboard/assets/index-BSjk0Sx9.js +634 -0
- package/assets/dashboard/assets/index-kRCYVN2B.css +1 -0
- package/assets/dashboard/index.html +2 -2
- package/assets/dashboard/noResult.svg +43 -0
- package/assets/dashboard/working.svg +1 -0
- package/migrations/0000_v040_base.sql +27 -0
- package/package.json +13 -9
- package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
- package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
- package/src/auth/bcrypt-hash-provider.ts +6 -2
- package/src/auth/constants.ts +4 -0
- package/src/auth/generate-refresh-token.test.ts +8 -4
- package/src/auth/hash-provider.test.ts +14 -5
- package/src/auth/jose-token-service.ts +7 -0
- package/src/auth/jwt-claims-passthrough.test.ts +87 -0
- package/src/auth/login.test.ts +9 -1
- package/src/auth/login.ts +5 -1
- package/src/auth/refresh.ts +7 -1
- package/src/auth/token-service.test.ts +9 -1
- package/src/factory.ts +37 -16
- package/src/features/automations/__tests__/action-executors.test.ts +59 -72
- package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
- package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
- package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
- package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
- package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
- package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
- package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
- package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
- package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
- package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
- package/src/features/automations/action-executors/index.ts +5 -1
- package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
- package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
- package/src/features/automations/action-executors/webhook.executor.ts +55 -13
- package/src/features/automations/automation-runner.ts +4 -0
- package/src/features/automations/automation-runner.utils.ts +4 -0
- package/src/features/automations/automations.handler.ts +4 -0
- package/src/features/automations/automations.schema.ts +19 -2
- package/src/features/automations/context-resolver.ts +4 -0
- package/src/features/automations/cron-runner.ts +4 -0
- package/src/features/automations/cron-runner.utils.ts +4 -0
- package/src/features/automations/filter-translation.ts +4 -0
- package/src/features/automations/index.ts +4 -0
- package/src/features/automations/template-grammar.ts +4 -0
- package/src/features/automations/var-access-resolver.ts +4 -0
- package/src/features/automations/when-evaluator.ts +4 -0
- package/src/features/automations/when-pushdown.ts +4 -0
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
- package/src/features/backrefs/backrefs.handler.ts +168 -0
- package/src/features/backrefs/d1-backref.repository.ts +84 -0
- package/src/features/backrefs/index.ts +5 -0
- package/src/features/content/constants.ts +6 -0
- package/src/features/content/handlers/bulk.handler.ts +185 -0
- package/src/features/content/handlers/create.ts +19 -55
- package/src/features/content/handlers/delete.ts +12 -37
- package/src/features/content/handlers/facets.ts +4 -0
- package/src/features/content/handlers/get.ts +4 -0
- package/src/features/content/handlers/helpers.test.ts +180 -0
- package/src/features/content/handlers/helpers.ts +93 -0
- package/src/features/content/handlers/list.ts +105 -19
- package/src/features/content/handlers/update.ts +19 -64
- package/src/features/content/index.ts +6 -0
- package/src/features/draft/draft.handler.ts +33 -8
- package/src/features/draft/draft.middleware.ts +4 -0
- package/src/features/draft/index.ts +4 -0
- package/src/features/email/email.provider.ts +4 -0
- package/src/features/email/email.service.ts +50 -45
- package/src/features/email/email.types.ts +12 -1
- package/src/features/email/index.ts +4 -0
- package/src/features/email/providers/resend.ts +4 -0
- package/src/features/email/providers/smtp.ts +55 -0
- package/src/features/email/templates/automation-mail.ts +4 -0
- package/src/features/email/templates/password-changed.ts +4 -0
- package/src/features/email/templates/password-reset.ts +4 -0
- package/src/features/email/templates/shell.ts +4 -0
- package/src/features/notifications/index.ts +4 -0
- package/src/features/notifications/notifications.handler.ts +4 -0
- package/src/features/password-reset/index.ts +5 -1
- package/src/features/password-reset/request.ts +12 -2
- package/src/features/password-reset/reset.ts +12 -2
- package/src/features/rotate-field/index.ts +4 -0
- package/src/features/rotate-field/rotate-field.handler.ts +4 -0
- package/src/features/rotate-field/rotate-field.schema.ts +4 -0
- package/src/features/schema/schema.handler.ts +118 -3
- package/src/features/seeds/index.ts +5 -0
- package/src/features/seeds/seeds.handler.test.ts +632 -0
- package/src/features/seeds/seeds.handler.ts +693 -0
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
- package/src/features/settings/settings.handler.ts +98 -8
- package/src/features/setup/index.ts +124 -11
- package/src/features/stats/index.ts +4 -0
- package/src/features/stats/stats.handler.ts +11 -21
- package/src/features/webhooks/index.ts +63 -0
- package/src/index.ts +28 -19
- package/src/media-utils.ts +4 -0
- package/src/middleware/auth-providers.middleware.ts +13 -0
- package/src/middleware/observability.middleware.ts +21 -3
- package/src/middleware/rate-limit.middleware.ts +4 -0
- package/src/middleware/repository.middleware.ts +25 -2
- package/src/middleware/seed-registry.middleware.test.ts +97 -0
- package/src/middleware/seed-registry.middleware.ts +21 -0
- package/src/middleware/storage.middleware.ts +4 -0
- package/src/middleware.ts +5 -7
- package/src/public/access-policy.ts +4 -0
- package/src/public/api-key-middleware.ts +4 -0
- package/src/public/cache-utils.ts +4 -0
- package/src/public/entry-projection.ts +4 -0
- package/src/public/idempotency.ts +4 -0
- package/src/public/index.ts +4 -0
- package/src/public/problem-details.ts +71 -0
- package/src/public/public-add.ts +4 -0
- package/src/public/public-edit.ts +4 -0
- package/src/public/public-errors.ts +4 -0
- package/src/public/public-read.ts +4 -0
- package/src/public/public-routes.ts +4 -0
- package/src/public/query-builder.test.ts +4 -0
- package/src/public/query-builder.ts +4 -0
- package/src/public/rate-limit-middleware.ts +4 -0
- package/src/public/read-list.ts +4 -0
- package/src/public/read-single.ts +4 -0
- package/src/public/response-builder.ts +4 -0
- package/src/public/sanitize.ts +4 -0
- package/src/public/slug-utils.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.ts +4 -0
- package/src/search-utils.test.ts +4 -0
- package/src/search-utils.ts +4 -0
- package/src/search.ts +4 -0
- package/src/shared/apply-policies.test.ts +4 -0
- package/src/shared/apply-policies.ts +4 -0
- package/src/shared/automations.repository.d1.ts +4 -0
- package/src/shared/background-notification-service.test.ts +4 -0
- package/src/shared/background-notification-service.ts +4 -0
- package/src/shared/base.repository.d1.ts +4 -0
- package/src/shared/content-utils.test.ts +4 -0
- package/src/shared/content-utils.ts +4 -0
- package/src/shared/content.repository.d1.test.ts +147 -1
- package/src/shared/content.repository.d1.ts +535 -66
- package/src/shared/d1-activity-log.repository.test.ts +4 -0
- package/src/shared/d1-activity-log.repository.ts +4 -0
- package/src/shared/d1-activity-logger.test.ts +4 -0
- package/src/shared/d1-activity-logger.ts +4 -0
- package/src/shared/d1-analytics.repository.test.ts +4 -0
- package/src/shared/d1-analytics.repository.ts +4 -0
- package/src/shared/d1-content-scan.repository.test.ts +9 -5
- package/src/shared/d1-content-scan.repository.ts +15 -7
- package/src/shared/d1-notification.repository.test.ts +4 -0
- package/src/shared/d1-notification.repository.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.ts +4 -0
- package/src/shared/d1-search.repository.test.ts +4 -0
- package/src/shared/d1-search.repository.ts +4 -0
- package/src/shared/d1-session.repository.test.ts +4 -0
- package/src/shared/d1-session.repository.ts +4 -0
- package/src/shared/d1-setup-checklist.repository.ts +36 -0
- package/src/shared/d1-user.repository.test.ts +8 -4
- package/src/shared/d1-user.repository.ts +15 -5
- package/src/shared/d1-widget.repository.test.ts +4 -0
- package/src/shared/d1-widget.repository.ts +4 -0
- package/src/shared/demo-data-sql.ts +54 -0
- package/src/shared/demo-data.repository.d1.ts +20 -0
- package/src/shared/execution-context-scheduler.ts +4 -0
- package/src/shared/fixed-clock.ts +4 -0
- package/src/shared/fts-sync.ts +4 -0
- package/src/shared/idempotency.repository.d1.test.ts +4 -0
- package/src/shared/idempotency.repository.d1.ts +4 -0
- package/src/shared/in-memory-activity-logger.ts +4 -0
- package/src/shared/in-memory-notification-service.ts +4 -0
- package/src/shared/in-memory-seed.repository.ts +51 -0
- package/src/shared/media.repository.d1.test.ts +4 -0
- package/src/shared/media.repository.d1.ts +4 -0
- package/src/shared/qstash-notification-service.test.ts +67 -0
- package/src/shared/qstash-notification-service.ts +55 -0
- package/src/shared/query-utils.ts +4 -0
- package/src/shared/request-utils.ts +4 -0
- package/src/shared/schema-mutator.d1.ts +64 -0
- package/src/shared/seed-layout.repository.d1.test.ts +114 -0
- package/src/shared/seed-layout.repository.d1.ts +62 -0
- package/src/shared/seed-registry-cache.test.ts +68 -0
- package/src/shared/seed-registry-cache.ts +49 -0
- package/src/shared/seed.repository.d1.test.ts +143 -0
- package/src/shared/seed.repository.d1.ts +98 -0
- package/src/shared/sequential-id-generator.ts +11 -0
- package/src/shared/site-settings.repository.d1.ts +53 -0
- package/src/shared/storage/factory.ts +16 -15
- package/src/shared/storage/s3-bucket.ts +34 -2
- package/src/shared/storage-utils.ts +4 -0
- package/src/shared/system-stats.repository.d1.test.ts +4 -0
- package/src/shared/system-stats.repository.d1.ts +5 -1
- package/src/types.ts +23 -3
- package/src/upload.ts +134 -123
- package/src/widget.ts +6 -2
- package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
- package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
- package/src/shared/storage/r2-binding-bucket.ts +0 -81
|
@@ -1,6 +1,10 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
6
|
import { D1ContentRepository } from './content.repository.d1'
|
|
3
|
-
import { EntryNotFoundError, SlugConflictError } from '@beechcms/core'
|
|
7
|
+
import { EntryNotFoundError, SlugConflictError, RelationTargetNotFoundError } from '@beechcms/core'
|
|
4
8
|
import type { Seed } from '@beechcms/core'
|
|
5
9
|
|
|
6
10
|
const SEED = {
|
|
@@ -20,6 +24,17 @@ const NO_DRAFT_SEED = {
|
|
|
20
24
|
branches: [{ id: 'br_01', alias: 'name', type: 'text' }],
|
|
21
25
|
} as unknown as Seed
|
|
22
26
|
|
|
27
|
+
// Seed with a multi-relation branch
|
|
28
|
+
const M2M_SEED = {
|
|
29
|
+
slug: 'articles',
|
|
30
|
+
displayNameAlias: 'title',
|
|
31
|
+
allowDrafts: true,
|
|
32
|
+
branches: [
|
|
33
|
+
{ id: 'br_01', alias: 'title', type: 'text' },
|
|
34
|
+
{ id: 'br_02', alias: 'tags', type: 'relation', multiple: true, targetSeed: 'tag' },
|
|
35
|
+
],
|
|
36
|
+
} as unknown as Seed
|
|
37
|
+
|
|
23
38
|
function makeMockDb(opts: {
|
|
24
39
|
firstResult?: unknown
|
|
25
40
|
allResults?: unknown[]
|
|
@@ -309,4 +324,135 @@ describe('D1ContentRepository', () => {
|
|
|
309
324
|
expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('content_posts_drafts'))
|
|
310
325
|
})
|
|
311
326
|
})
|
|
327
|
+
|
|
328
|
+
// ─── many-to-many (Sprint 5) ──────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
describe('many-to-many relations', () => {
|
|
331
|
+
it('create: multi-relation values go to junction table, not main INSERT', async () => {
|
|
332
|
+
const { db, prepareMock, batchMock } = makeMockDb({ firstResult: null })
|
|
333
|
+
batchMock.mockResolvedValue([{ success: true, meta: { changes: 1 } }])
|
|
334
|
+
|
|
335
|
+
await new D1ContentRepository(db).create(
|
|
336
|
+
M2M_SEED, 'e1', 'art-1', 'draft',
|
|
337
|
+
{ title: 'Hello', tags: ['tag-1', 'tag-2'] },
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
341
|
+
// Main INSERT must not contain 'tags'
|
|
342
|
+
const insertSql = sqls.find(s => s.includes('INSERT INTO content_articles '))
|
|
343
|
+
expect(insertSql).toBeDefined()
|
|
344
|
+
expect(insertSql).not.toContain('tags')
|
|
345
|
+
// Junction INSERTs must exist
|
|
346
|
+
const junctionInserts = sqls.filter(s => s.includes('rel_articles_tags'))
|
|
347
|
+
expect(junctionInserts.length).toBeGreaterThan(0)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('create: uses batch when multi-relation values present', async () => {
|
|
351
|
+
const { db, batchMock } = makeMockDb({ firstResult: null })
|
|
352
|
+
batchMock.mockResolvedValue([{ success: true }])
|
|
353
|
+
|
|
354
|
+
await new D1ContentRepository(db).create(
|
|
355
|
+
M2M_SEED, 'e1', 'art-1', 'draft',
|
|
356
|
+
{ title: 'Hello', tags: ['tag-1'] },
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
expect(batchMock).toHaveBeenCalled()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('update: replaces junction rows (DELETE + INSERT) in batch', async () => {
|
|
363
|
+
const { db, prepareMock, batchMock } = makeMockDb({ runChanges: 1 })
|
|
364
|
+
batchMock.mockResolvedValue([
|
|
365
|
+
{ meta: { changes: 1 } },
|
|
366
|
+
{ success: true },
|
|
367
|
+
{ success: true },
|
|
368
|
+
])
|
|
369
|
+
|
|
370
|
+
await new D1ContentRepository(db).update(
|
|
371
|
+
M2M_SEED, 'e1',
|
|
372
|
+
{ tags: ['tag-3', 'tag-4'] },
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
376
|
+
expect(sqls.some(s => s.includes('DELETE FROM rel_articles_tags WHERE parent_id'))).toBe(true)
|
|
377
|
+
expect(sqls.some(s => s.includes('INSERT INTO rel_articles_tags'))).toBe(true)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('update: empty array deletes all junction rows', async () => {
|
|
381
|
+
const { db, prepareMock, batchMock } = makeMockDb({ runChanges: 1 })
|
|
382
|
+
batchMock.mockResolvedValue([{ meta: { changes: 1 } }, { success: true }])
|
|
383
|
+
|
|
384
|
+
await new D1ContentRepository(db).update(M2M_SEED, 'e1', { tags: [] })
|
|
385
|
+
|
|
386
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
387
|
+
expect(sqls.some(s => s.includes('DELETE FROM rel_articles_tags WHERE parent_id'))).toBe(true)
|
|
388
|
+
// No INSERT because array is empty
|
|
389
|
+
expect(sqls.filter(s => s.includes('INSERT INTO rel_articles_tags'))).toHaveLength(0)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('findById: attaches multi-relation ids from junction table', async () => {
|
|
393
|
+
const row = { id: 'e1', slug: 'art-1', status: 'published', title: 'Hello', created_at: 0, updated_at: 0 }
|
|
394
|
+
const { db, firstMock, batchMock } = makeMockDb({ firstResult: row })
|
|
395
|
+
// First call: main SELECT; batch: junction query
|
|
396
|
+
batchMock.mockResolvedValueOnce([{ results: [{ target_id: 'tag-1' }, { target_id: 'tag-2' }] }])
|
|
397
|
+
|
|
398
|
+
const result = await new D1ContentRepository(db).findById(M2M_SEED, 'e1')
|
|
399
|
+
expect(result.tags).toEqual(['tag-1', 'tag-2'])
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('findById: attaches empty array when no junction rows', async () => {
|
|
403
|
+
const row = { id: 'e1', slug: 'art-1', status: 'published', title: 'Hello', created_at: 0, updated_at: 0 }
|
|
404
|
+
const { db, batchMock } = makeMockDb({ firstResult: row })
|
|
405
|
+
batchMock.mockResolvedValueOnce([{ results: [] }])
|
|
406
|
+
|
|
407
|
+
const result = await new D1ContentRepository(db).findById(M2M_SEED, 'e1')
|
|
408
|
+
expect(result.tags).toEqual([])
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('findMany: attaches multi-relation ids per entry', async () => {
|
|
412
|
+
const rows = [
|
|
413
|
+
{ id: 'e1', slug: 'art-1', status: 'published', title: 'A', created_at: 0, updated_at: 0 },
|
|
414
|
+
{ id: 'e2', slug: 'art-2', status: 'published', title: 'B', created_at: 0, updated_at: 0 },
|
|
415
|
+
]
|
|
416
|
+
const { db, batchMock } = makeMockDb()
|
|
417
|
+
// First batch: main select + count; second batch: junction query
|
|
418
|
+
batchMock
|
|
419
|
+
.mockResolvedValueOnce([{ results: rows }, { results: [{ total: 2 }] }])
|
|
420
|
+
.mockResolvedValueOnce([{
|
|
421
|
+
results: [
|
|
422
|
+
{ parent_id: 'e1', target_id: 'tag-1' },
|
|
423
|
+
{ parent_id: 'e1', target_id: 'tag-2' },
|
|
424
|
+
{ parent_id: 'e2', target_id: 'tag-3' },
|
|
425
|
+
],
|
|
426
|
+
}])
|
|
427
|
+
|
|
428
|
+
const result = await new D1ContentRepository(db).findMany(M2M_SEED, {})
|
|
429
|
+
expect(result.items[0].tags).toEqual(['tag-1', 'tag-2'])
|
|
430
|
+
expect(result.items[1].tags).toEqual(['tag-3'])
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('saveDraft: multi-relation values go to junction draft table', async () => {
|
|
434
|
+
const { db, prepareMock, batchMock } = makeMockDb()
|
|
435
|
+
batchMock.mockResolvedValue([{ success: true }])
|
|
436
|
+
|
|
437
|
+
await new D1ContentRepository(db).saveDraft(M2M_SEED, 'e1', { tags: ['tag-1', 'tag-2'] })
|
|
438
|
+
|
|
439
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
440
|
+
expect(sqls.some(s => s.includes('rel_articles_tags_drafts'))).toBe(true)
|
|
441
|
+
expect(batchMock).toHaveBeenCalled()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('publishDraft: throws RelationTargetNotFoundError when draft multi-rel target missing', async () => {
|
|
445
|
+
const draftRow = { entry_id: 'e1', title: 'Hello' }
|
|
446
|
+
const { db, firstMock, allMock, batchMock } = makeMockDb()
|
|
447
|
+
firstMock
|
|
448
|
+
.mockResolvedValueOnce(draftRow) // getDraft row
|
|
449
|
+
.mockResolvedValueOnce(null) // target existence check → not found
|
|
450
|
+
allMock.mockResolvedValueOnce({ results: [{ target_id: 'missing-tag' }] })
|
|
451
|
+
batchMock.mockResolvedValue([{}])
|
|
452
|
+
|
|
453
|
+
await expect(
|
|
454
|
+
new D1ContentRepository(db).publishDraft(M2M_SEED, 'e1'),
|
|
455
|
+
).rejects.toBeInstanceOf(RelationTargetNotFoundError)
|
|
456
|
+
})
|
|
457
|
+
})
|
|
312
458
|
})
|