@beechcms/api 0.4.0-preview.11 → 0.4.0-preview.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/assets/index-CewtCjom.css +1 -0
  4. package/assets/dashboard/assets/index-FQ6JhvRH.js +554 -0
  5. package/assets/dashboard/beechLogoDark.svg +48 -48
  6. package/assets/dashboard/index.html +18 -18
  7. package/assets/dashboard/sol.svg +3 -3
  8. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  9. package/migrations/0000_v040_base.sql +213 -213
  10. package/package.json +2 -2
  11. package/src/auth/constants.ts +10 -10
  12. package/src/auth/login.ts +91 -91
  13. package/src/auth/refresh.ts +127 -127
  14. package/src/factory.ts +347 -325
  15. package/src/features/content/constants.ts +10 -10
  16. package/src/features/content/handlers/create.ts +153 -163
  17. package/src/features/content/handlers/delete.ts +76 -85
  18. package/src/features/content/handlers/facets.ts +45 -45
  19. package/src/features/content/handlers/get.ts +116 -116
  20. package/src/features/content/handlers/list.ts +88 -88
  21. package/src/features/content/handlers/update.ts +207 -216
  22. package/src/features/content/index.ts +20 -20
  23. package/src/features/draft/draft.handler.ts +272 -272
  24. package/src/features/draft/index.ts +1 -1
  25. package/src/features/email/email.provider.ts +38 -38
  26. package/src/features/email/email.service.ts +80 -80
  27. package/src/features/email/email.types.ts +98 -98
  28. package/src/features/email/index.ts +28 -28
  29. package/src/features/email/providers/resend.ts +63 -63
  30. package/src/features/email/templates/password-changed.ts +59 -59
  31. package/src/features/email/templates/password-reset.ts +64 -64
  32. package/src/features/email/templates/shell.ts +92 -93
  33. package/src/features/notifications/index.ts +1 -1
  34. package/src/features/notifications/notifications.handler.ts +130 -88
  35. package/src/features/password-reset/index.ts +15 -15
  36. package/src/features/password-reset/request.ts +106 -88
  37. package/src/features/password-reset/reset.ts +128 -110
  38. package/src/features/rotate-field/index.ts +1 -1
  39. package/src/features/rotate-field/rotate-field.handler.ts +132 -82
  40. package/src/features/rotate-field/rotate-field.schema.ts +13 -9
  41. package/src/features/schema/schema.handler.ts +16 -16
  42. package/src/features/settings/settings.handler.ts +414 -267
  43. package/src/features/setup/index.ts +96 -59
  44. package/src/features/stats/index.ts +1 -1
  45. package/src/features/stats/stats.handler.ts +458 -395
  46. package/src/index.ts +24 -24
  47. package/src/media-utils.ts +78 -78
  48. package/src/middleware/repository.middleware.ts +24 -18
  49. package/src/middleware/storage.middleware.ts +21 -0
  50. package/src/middleware.ts +67 -67
  51. package/src/public/access-policy.ts +23 -23
  52. package/src/public/api-key-middleware.ts +53 -53
  53. package/src/public/index.ts +12 -12
  54. package/src/public/problem-details.ts +48 -42
  55. package/src/public/public-add.ts +166 -166
  56. package/src/public/public-edit.ts +158 -158
  57. package/src/public/public-errors.ts +15 -15
  58. package/src/public/public-read.ts +216 -216
  59. package/src/public/public-routes.ts +84 -31
  60. package/src/public/query-builder.ts +152 -152
  61. package/src/public/rate-limit-middleware.ts +42 -42
  62. package/src/public/response-builder.ts +26 -26
  63. package/src/public/sanitize.ts +65 -65
  64. package/src/public/slug-utils.ts +14 -14
  65. package/src/search-utils.ts +192 -192
  66. package/src/search.ts +72 -72
  67. package/src/shared/activity-logger.ts +79 -79
  68. package/src/shared/apply-policies.ts +63 -63
  69. package/src/shared/base.repository.d1.ts +28 -28
  70. package/src/shared/content-utils.ts +82 -108
  71. package/src/shared/content.repository.d1.ts +382 -382
  72. package/src/shared/fts-sync.ts +4 -4
  73. package/src/shared/idempotency.repository.d1.ts +56 -45
  74. package/src/shared/media.repository.d1.ts +64 -0
  75. package/src/shared/notification-service.ts +56 -56
  76. package/src/shared/query-utils.ts +137 -137
  77. package/src/shared/storage/factory.ts +40 -0
  78. package/src/shared/storage/r2-binding-bucket.ts +81 -0
  79. package/src/shared/storage/s3-bucket.ts +163 -0
  80. package/src/shared/storage-utils.ts +36 -36
  81. package/src/shared/system-stats.repository.d1.ts +44 -0
  82. package/src/types.ts +46 -41
  83. package/src/upload.ts +179 -331
  84. package/src/widget.ts +349 -349
  85. package/assets/dashboard/assets/index-9Ch2xWJr.js +0 -554
  86. package/assets/dashboard/assets/index-ye3325L9.css +0 -1
@@ -1,116 +1,116 @@
1
- import { Context } from 'hono'
2
- import { applyVisibility } from '../../../shared/apply-policies'
3
- import { publicProblem } from '../../../public/problem-details'
4
- import { CONTENT_ERRORS } from '../constants'
5
- import { AppEnv } from '../../../types'
6
- import { EntryNotFoundError } from '@beechcms/core'
7
-
8
- export async function getByIdHandler(context: Context<AppEnv>) {
9
- const slug = context.req.param('slug')
10
- const id = context.req.param('id')
11
- if (!slug || !id) {
12
- return publicProblem(context, {
13
- type: 'content-invalid-slug-or-id',
14
- title: 'Bad Request',
15
- status: 400,
16
- detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
17
- })
18
- }
19
-
20
- const seed = context.get('getSeed')(slug)
21
- if (!seed) {
22
- return publicProblem(context, {
23
- type: 'content-seed-not-found',
24
- title: 'Not Found',
25
- status: 404,
26
- detail: CONTENT_ERRORS.SEED_NOT_FOUND
27
- })
28
- }
29
-
30
- try {
31
- const repository = context.get('repository')
32
- const item = await repository.findById(seed, id)
33
-
34
- let hasPendingDraft = false
35
- if (seed.allowDrafts) {
36
- hasPendingDraft = await repository.hasDraft(seed, id)
37
- }
38
-
39
- return context.json({
40
- ...item,
41
- has_pending_draft: hasPendingDraft,
42
- data: applyVisibility(item, seed)
43
- })
44
- } catch (error) {
45
- if (error instanceof EntryNotFoundError) {
46
- return publicProblem(context, {
47
- type: 'content-not-found',
48
- title: 'Not Found',
49
- status: 404,
50
- detail: CONTENT_ERRORS.NOT_FOUND
51
- })
52
- }
53
- console.error('Content detail error:', error)
54
- return publicProblem(context, {
55
- type: 'content-database-error',
56
- title: 'Internal Server Error',
57
- status: 500,
58
- detail: CONTENT_ERRORS.DATABASE_ERROR
59
- })
60
- }
61
- }
62
-
63
- export async function getBySlugHandler(context: Context<AppEnv>) {
64
- const schemaSlug = context.req.param('schema_slug')
65
- const entrySlug = context.req.param('entry_slug')
66
- if (!schemaSlug || !entrySlug) {
67
- return publicProblem(context, {
68
- type: 'content-invalid-slug-or-id',
69
- title: 'Bad Request',
70
- status: 400,
71
- detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
72
- })
73
- }
74
-
75
- const seed = context.get('getSeed')(schemaSlug)
76
- if (!seed) {
77
- return publicProblem(context, {
78
- type: 'content-seed-not-found',
79
- title: 'Not Found',
80
- status: 404,
81
- detail: CONTENT_ERRORS.SEED_NOT_FOUND
82
- })
83
- }
84
-
85
- try {
86
- const repository = context.get('repository')
87
- const item = await repository.findBySlug(seed, entrySlug)
88
-
89
- let hasPendingDraft = false
90
- if (seed.allowDrafts) {
91
- hasPendingDraft = await repository.hasDraft(seed, item.id)
92
- }
93
-
94
- return context.json({
95
- ...item,
96
- has_pending_draft: hasPendingDraft,
97
- data: applyVisibility(item, seed)
98
- })
99
- } catch (error) {
100
- if (error instanceof EntryNotFoundError) {
101
- return publicProblem(context, {
102
- type: 'content-not-found',
103
- title: 'Not Found',
104
- status: 404,
105
- detail: CONTENT_ERRORS.NOT_FOUND
106
- })
107
- }
108
- console.error('Content by-slug error:', error)
109
- return publicProblem(context, {
110
- type: 'content-database-error',
111
- title: 'Internal Server Error',
112
- status: 500,
113
- detail: CONTENT_ERRORS.DATABASE_ERROR
114
- })
115
- }
116
- }
1
+ import { Context } from 'hono'
2
+ import { applyVisibility } from '../../../shared/apply-policies'
3
+ import { publicProblem } from '../../../public/problem-details'
4
+ import { CONTENT_ERRORS } from '../constants'
5
+ import { AppEnv } from '../../../types'
6
+ import { EntryNotFoundError } from '@beechcms/core'
7
+
8
+ export async function getByIdHandler(context: Context<AppEnv>) {
9
+ const slug = context.req.param('slug')
10
+ const id = context.req.param('id')
11
+ if (!slug || !id) {
12
+ return publicProblem(context, {
13
+ type: 'content-invalid-slug-or-id',
14
+ title: 'Bad Request',
15
+ status: 400,
16
+ detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
17
+ })
18
+ }
19
+
20
+ const seed = context.get('getSeed')(slug)
21
+ if (!seed) {
22
+ return publicProblem(context, {
23
+ type: 'content-seed-not-found',
24
+ title: 'Not Found',
25
+ status: 404,
26
+ detail: CONTENT_ERRORS.SEED_NOT_FOUND
27
+ })
28
+ }
29
+
30
+ try {
31
+ const repository = context.get('repository')
32
+ const item = await repository.findById(seed, id)
33
+
34
+ let hasPendingDraft = false
35
+ if (seed.allowDrafts) {
36
+ hasPendingDraft = await repository.hasDraft(seed, id)
37
+ }
38
+
39
+ return context.json({
40
+ ...item,
41
+ has_pending_draft: hasPendingDraft,
42
+ data: applyVisibility(item, seed)
43
+ })
44
+ } catch (error) {
45
+ if (error instanceof EntryNotFoundError) {
46
+ return publicProblem(context, {
47
+ type: 'content-not-found',
48
+ title: 'Not Found',
49
+ status: 404,
50
+ detail: CONTENT_ERRORS.NOT_FOUND
51
+ })
52
+ }
53
+ console.error('Content detail error:', error)
54
+ return publicProblem(context, {
55
+ type: 'content-database-error',
56
+ title: 'Internal Server Error',
57
+ status: 500,
58
+ detail: CONTENT_ERRORS.DATABASE_ERROR
59
+ })
60
+ }
61
+ }
62
+
63
+ export async function getBySlugHandler(context: Context<AppEnv>) {
64
+ const schemaSlug = context.req.param('schema_slug')
65
+ const entrySlug = context.req.param('entry_slug')
66
+ if (!schemaSlug || !entrySlug) {
67
+ return publicProblem(context, {
68
+ type: 'content-invalid-slug-or-id',
69
+ title: 'Bad Request',
70
+ status: 400,
71
+ detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
72
+ })
73
+ }
74
+
75
+ const seed = context.get('getSeed')(schemaSlug)
76
+ if (!seed) {
77
+ return publicProblem(context, {
78
+ type: 'content-seed-not-found',
79
+ title: 'Not Found',
80
+ status: 404,
81
+ detail: CONTENT_ERRORS.SEED_NOT_FOUND
82
+ })
83
+ }
84
+
85
+ try {
86
+ const repository = context.get('repository')
87
+ const item = await repository.findBySlug(seed, entrySlug)
88
+
89
+ let hasPendingDraft = false
90
+ if (seed.allowDrafts) {
91
+ hasPendingDraft = await repository.hasDraft(seed, item.id)
92
+ }
93
+
94
+ return context.json({
95
+ ...item,
96
+ has_pending_draft: hasPendingDraft,
97
+ data: applyVisibility(item, seed)
98
+ })
99
+ } catch (error) {
100
+ if (error instanceof EntryNotFoundError) {
101
+ return publicProblem(context, {
102
+ type: 'content-not-found',
103
+ title: 'Not Found',
104
+ status: 404,
105
+ detail: CONTENT_ERRORS.NOT_FOUND
106
+ })
107
+ }
108
+ console.error('Content by-slug error:', error)
109
+ return publicProblem(context, {
110
+ type: 'content-database-error',
111
+ title: 'Internal Server Error',
112
+ status: 500,
113
+ detail: CONTENT_ERRORS.DATABASE_ERROR
114
+ })
115
+ }
116
+ }
@@ -1,88 +1,88 @@
1
- import { Context } from 'hono'
2
- import { parsePositiveInt, parseQueryFilters, cleanStr, toEngineFilters } from '../../../shared/query-utils'
3
- import { applyVisibility } from '../../../shared/apply-policies'
4
- import { publicProblem } from '../../../public/problem-details'
5
- import { CONTENT_ERRORS } from '../constants'
6
- import { AppEnv } from '../../../types'
7
-
8
- export async function listHandler(context: Context<AppEnv>) {
9
- const slug = context.req.param('slug')
10
- if (!slug) {
11
- return publicProblem(context, {
12
- type: 'content-invalid-slug',
13
- title: 'Bad Request',
14
- status: 400,
15
- detail: CONTENT_ERRORS.INVALID_SLUG
16
- })
17
- }
18
-
19
- const seed = context.get('getSeed')(slug)
20
- if (!seed) {
21
- return publicProblem(context, {
22
- type: 'content-seed-not-found',
23
- title: 'Not Found',
24
- status: 404,
25
- detail: CONTENT_ERRORS.SEED_NOT_FOUND
26
- })
27
- }
28
-
29
- try {
30
- const query = context.req.query()
31
- const search = cleanStr(query.search) ?? ''
32
- const sortBy = cleanStr(query.sortBy) ?? ''
33
- const sortDirRaw = cleanStr(query.sortDir)?.toLowerCase() ?? 'asc'
34
- const rawFilters = parseQueryFilters(query.filters)
35
- const engineFilters = toEngineFilters(rawFilters)
36
-
37
- // Note: repository doesn't yet support has_pending_draft filter/column natively
38
- // in findMany without SQL manipulation. We'll stick to basic findMany for now
39
- // and might need to enhance the repository if this feature is critical for v1 of this refactor.
40
- // The legacy code was doing a lot of SQL injection here.
41
-
42
- const page = parsePositiveInt(query.page, 1)
43
- const limit = Math.min(parsePositiveInt(query.limit, 25), 100)
44
- const offset = (page - 1) * limit
45
-
46
- const orderBy = sortBy
47
- ? { column: sortBy, dir: (sortDirRaw === 'desc' ? 'DESC' : 'ASC') as 'ASC' | 'DESC' }
48
- : undefined
49
-
50
- const repository = context.get('repository')
51
- const { items, total } = await repository.findMany(seed, {
52
- filters: engineFilters,
53
- orderBy,
54
- search: search || undefined,
55
- pagination: { limit, offset },
56
- })
57
-
58
- const entries = await Promise.all(items.map(async (item) => {
59
- // Check for pending draft if allowed
60
- let hasPendingDraft = false
61
- if (seed.allowDrafts) {
62
- hasPendingDraft = await repository.hasDraft(seed, item.id)
63
- }
64
-
65
- return {
66
- ...item,
67
- has_pending_draft: hasPendingDraft,
68
- data: applyVisibility(item, seed) // Repository returns "pure" data including system fields
69
- }
70
- }))
71
-
72
- // If no query params (except slug), return array directly (legacy compatibility)
73
- const hasQueryParams = Boolean(search) || Boolean(sortBy) || Boolean(query.filters) || query.page !== undefined || query.limit !== undefined
74
- if (!hasQueryParams) {
75
- return context.json(entries)
76
- }
77
-
78
- return context.json({ items: entries, total, page, limit })
79
- } catch (error) {
80
- console.error('Content list error:', error)
81
- return publicProblem(context, {
82
- type: 'content-database-error',
83
- title: 'Internal Server Error',
84
- status: 500,
85
- detail: CONTENT_ERRORS.DATABASE_ERROR
86
- })
87
- }
88
- }
1
+ import { Context } from 'hono'
2
+ import { parsePositiveInt, parseQueryFilters, cleanStr, toEngineFilters } from '../../../shared/query-utils'
3
+ import { applyVisibility } from '../../../shared/apply-policies'
4
+ import { publicProblem } from '../../../public/problem-details'
5
+ import { CONTENT_ERRORS } from '../constants'
6
+ import { AppEnv } from '../../../types'
7
+
8
+ export async function listHandler(context: Context<AppEnv>) {
9
+ const slug = context.req.param('slug')
10
+ if (!slug) {
11
+ return publicProblem(context, {
12
+ type: 'content-invalid-slug',
13
+ title: 'Bad Request',
14
+ status: 400,
15
+ detail: CONTENT_ERRORS.INVALID_SLUG
16
+ })
17
+ }
18
+
19
+ const seed = context.get('getSeed')(slug)
20
+ if (!seed) {
21
+ return publicProblem(context, {
22
+ type: 'content-seed-not-found',
23
+ title: 'Not Found',
24
+ status: 404,
25
+ detail: CONTENT_ERRORS.SEED_NOT_FOUND
26
+ })
27
+ }
28
+
29
+ try {
30
+ const query = context.req.query()
31
+ const search = cleanStr(query.search) ?? ''
32
+ const sortBy = cleanStr(query.sortBy) ?? ''
33
+ const sortDirRaw = cleanStr(query.sortDir)?.toLowerCase() ?? 'asc'
34
+ const rawFilters = parseQueryFilters(query.filters)
35
+ const engineFilters = toEngineFilters(rawFilters)
36
+
37
+ // Note: repository doesn't yet support has_pending_draft filter/column natively
38
+ // in findMany without SQL manipulation. We'll stick to basic findMany for now
39
+ // and might need to enhance the repository if this feature is critical for v1 of this refactor.
40
+ // The legacy code was doing a lot of SQL injection here.
41
+
42
+ const page = parsePositiveInt(query.page, 1)
43
+ const limit = Math.min(parsePositiveInt(query.limit, 25), 100)
44
+ const offset = (page - 1) * limit
45
+
46
+ const orderBy = sortBy
47
+ ? { column: sortBy, dir: (sortDirRaw === 'desc' ? 'DESC' : 'ASC') as 'ASC' | 'DESC' }
48
+ : undefined
49
+
50
+ const repository = context.get('repository')
51
+ const { items, total } = await repository.findMany(seed, {
52
+ filters: engineFilters,
53
+ orderBy,
54
+ search: search || undefined,
55
+ pagination: { limit, offset },
56
+ })
57
+
58
+ const entries = await Promise.all(items.map(async (item) => {
59
+ // Check for pending draft if allowed
60
+ let hasPendingDraft = false
61
+ if (seed.allowDrafts) {
62
+ hasPendingDraft = await repository.hasDraft(seed, item.id)
63
+ }
64
+
65
+ return {
66
+ ...item,
67
+ has_pending_draft: hasPendingDraft,
68
+ data: applyVisibility(item, seed) // Repository returns "pure" data including system fields
69
+ }
70
+ }))
71
+
72
+ // If no query params (except slug), return array directly (legacy compatibility)
73
+ const hasQueryParams = Boolean(search) || Boolean(sortBy) || Boolean(query.filters) || query.page !== undefined || query.limit !== undefined
74
+ if (!hasQueryParams) {
75
+ return context.json(entries)
76
+ }
77
+
78
+ return context.json({ items: entries, total, page, limit })
79
+ } catch (error) {
80
+ console.error('Content list error:', error)
81
+ return publicProblem(context, {
82
+ type: 'content-database-error',
83
+ title: 'Internal Server Error',
84
+ status: 500,
85
+ detail: CONTENT_ERRORS.DATABASE_ERROR
86
+ })
87
+ }
88
+ }