@createcms/core 0.1.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.
Files changed (83) hide show
  1. package/README.md +169 -0
  2. package/dist/ab-edge/index.cjs +214 -0
  3. package/dist/ab-edge/index.d.cts +121 -0
  4. package/dist/ab-edge/index.d.ts +121 -0
  5. package/dist/ab-edge/index.js +205 -0
  6. package/dist/bin/createcms.js +3082 -0
  7. package/dist/db.cjs +496 -0
  8. package/dist/db.d.cts +128 -0
  9. package/dist/db.d.ts +128 -0
  10. package/dist/db.js +488 -0
  11. package/dist/index.cjs +13789 -0
  12. package/dist/index.d.cts +10277 -0
  13. package/dist/index.d.ts +10277 -0
  14. package/dist/index.js +13737 -0
  15. package/dist/nanoid.cjs +50 -0
  16. package/dist/nanoid.d.cts +29 -0
  17. package/dist/nanoid.d.ts +29 -0
  18. package/dist/nanoid.js +47 -0
  19. package/dist/next/index.cjs +60 -0
  20. package/dist/next/index.d.cts +141 -0
  21. package/dist/next/index.d.ts +141 -0
  22. package/dist/next/index.js +58 -0
  23. package/dist/next/middleware.cjs +113 -0
  24. package/dist/next/middleware.d.cts +77 -0
  25. package/dist/next/middleware.d.ts +77 -0
  26. package/dist/next/middleware.js +111 -0
  27. package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
  28. package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
  29. package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
  30. package/dist/plugins/ab-test/analytics/upstash.js +343 -0
  31. package/dist/plugins/ab-test/client.cjs +686 -0
  32. package/dist/plugins/ab-test/client.d.cts +233 -0
  33. package/dist/plugins/ab-test/client.d.ts +233 -0
  34. package/dist/plugins/ab-test/client.js +684 -0
  35. package/dist/plugins/ab-test/index.cjs +3400 -0
  36. package/dist/plugins/ab-test/index.d.cts +1131 -0
  37. package/dist/plugins/ab-test/index.d.ts +1131 -0
  38. package/dist/plugins/ab-test/index.js +3367 -0
  39. package/dist/plugins/client.cjs +20 -0
  40. package/dist/plugins/client.d.cts +3 -0
  41. package/dist/plugins/client.d.ts +3 -0
  42. package/dist/plugins/client.js +3 -0
  43. package/dist/plugins/consent/client.cjs +315 -0
  44. package/dist/plugins/consent/client.d.cts +145 -0
  45. package/dist/plugins/consent/client.d.ts +145 -0
  46. package/dist/plugins/consent/client.js +313 -0
  47. package/dist/plugins/consent/index.cjs +267 -0
  48. package/dist/plugins/consent/index.d.cts +618 -0
  49. package/dist/plugins/consent/index.d.ts +618 -0
  50. package/dist/plugins/consent/index.js +258 -0
  51. package/dist/plugins/i18n/index.cjs +2177 -0
  52. package/dist/plugins/i18n/index.d.cts +562 -0
  53. package/dist/plugins/i18n/index.d.ts +562 -0
  54. package/dist/plugins/i18n/index.js +2150 -0
  55. package/dist/plugins/media-optimize/index.cjs +315 -0
  56. package/dist/plugins/media-optimize/index.d.cts +144 -0
  57. package/dist/plugins/media-optimize/index.d.ts +144 -0
  58. package/dist/plugins/media-optimize/index.js +311 -0
  59. package/dist/plugins/multi-tenant/index.cjs +210 -0
  60. package/dist/plugins/multi-tenant/index.d.cts +431 -0
  61. package/dist/plugins/multi-tenant/index.d.ts +431 -0
  62. package/dist/plugins/multi-tenant/index.js +207 -0
  63. package/dist/plugins/server.cjs +24 -0
  64. package/dist/plugins/server.d.cts +3 -0
  65. package/dist/plugins/server.d.ts +3 -0
  66. package/dist/plugins/server.js +3 -0
  67. package/dist/react/blocks.cjs +233 -0
  68. package/dist/react/blocks.d.cts +320 -0
  69. package/dist/react/blocks.d.ts +320 -0
  70. package/dist/react/blocks.js +226 -0
  71. package/dist/react/index.cjs +901 -0
  72. package/dist/react/index.d.cts +992 -0
  73. package/dist/react/index.d.ts +992 -0
  74. package/dist/react/index.js +872 -0
  75. package/dist/react/tracking.cjs +243 -0
  76. package/dist/react/tracking.d.cts +364 -0
  77. package/dist/react/tracking.d.ts +364 -0
  78. package/dist/react/tracking.js +216 -0
  79. package/dist/react/variant.cjs +59 -0
  80. package/dist/react/variant.d.cts +26 -0
  81. package/dist/react/variant.d.ts +26 -0
  82. package/dist/react/variant.js +57 -0
  83. package/package.json +303 -0
@@ -0,0 +1,343 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { customAlphabet } from 'nanoid';
3
+
4
+ const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 20);
5
+ const prefixes = {
6
+ root: 'rot',
7
+ commit: 'cmt',
8
+ branch: 'brn',
9
+ blockVersion: 'blv',
10
+ block: 'blk',
11
+ mergeRequest: 'mrq',
12
+ mergeConflict: 'mcf',
13
+ approval: 'apr',
14
+ assetFolder: 'afl',
15
+ asset: 'ast',
16
+ contentUsage: 'cus',
17
+ commentThread: 'cth',
18
+ commentMessage: 'cmg',
19
+ commentMention: 'cmn',
20
+ variable: 'var',
21
+ template: 'tpl',
22
+ tplVarUsage: 'tvu',
23
+ notification: 'ntf',
24
+ si: 'sid',
25
+ redirect: 'rdr'
26
+ };
27
+ const customPrefixes = new Map();
28
+ function newId(prefix) {
29
+ const resolved = prefixes[prefix] ?? customPrefixes.get(prefix);
30
+ if (!resolved) {
31
+ throw new Error(`Unknown ID prefix "${prefix}". Register it with registerIdPrefix() first.`);
32
+ }
33
+ return `${resolved}_${nanoid()}`;
34
+ }
35
+
36
+ // Prevent Turbopack/webpack from statically analyzing these imports.
37
+ // The bundler rewrites bare `import('...')` calls into require/resolve
38
+ // that fail when the package lives in a different workspace. By
39
+ // constructing the specifier at runtime the import stays a true
40
+ // dynamic import that Node resolves at execution time.
41
+ const _upstashRedisId = [
42
+ '@upstash',
43
+ 'redis'
44
+ ].join('/');
45
+ const _upstashRealtimeId = [
46
+ '@upstash',
47
+ 'realtime'
48
+ ].join('/');
49
+ const _importUpstashRedis = ()=>new Function('id', 'return import(id)')(_upstashRedisId);
50
+ const _importUpstashRealtime = ()=>new Function('id', 'return import(id)')(_upstashRealtimeId);
51
+ const aggregationsTable = {
52
+ tableName: 'ab_test_aggregations',
53
+ indexPrefix: 'aba',
54
+ columns: {
55
+ id: {
56
+ type: 'text',
57
+ primaryKey: true,
58
+ defaultId: true,
59
+ defaultIdPrefix: 'abTestAgg'
60
+ },
61
+ testId: {
62
+ type: 'text',
63
+ notNull: true,
64
+ references: {
65
+ table: 'abTests',
66
+ column: 'id',
67
+ onDelete: 'cascade'
68
+ }
69
+ },
70
+ variantId: {
71
+ type: 'text',
72
+ notNull: true,
73
+ references: {
74
+ table: 'abTestVariants',
75
+ column: 'id',
76
+ onDelete: 'cascade'
77
+ }
78
+ },
79
+ eventType: {
80
+ type: 'text',
81
+ notNull: true
82
+ },
83
+ count: {
84
+ type: 'integer',
85
+ notNull: true,
86
+ default: {
87
+ kind: 'literal',
88
+ value: 0
89
+ }
90
+ },
91
+ uniqueVisitors: {
92
+ type: 'integer',
93
+ notNull: true,
94
+ default: {
95
+ kind: 'literal',
96
+ value: 0
97
+ }
98
+ },
99
+ periodStart: {
100
+ type: 'timestamp',
101
+ notNull: true
102
+ },
103
+ periodEnd: {
104
+ type: 'timestamp',
105
+ notNull: true
106
+ },
107
+ updatedAt: {
108
+ type: 'timestamp',
109
+ notNull: true,
110
+ defaultNow: true
111
+ }
112
+ },
113
+ indexes: {
114
+ testPeriodIdx: {
115
+ columns: [
116
+ 'testId',
117
+ 'periodStart'
118
+ ]
119
+ },
120
+ uniqueBucketIdx: {
121
+ columns: [
122
+ 'testId',
123
+ 'variantId',
124
+ 'eventType',
125
+ 'periodStart'
126
+ ],
127
+ unique: true
128
+ }
129
+ }
130
+ };
131
+ /**
132
+ * Upstash Redis Stream adapter with batch flush and realtime delta publishing.
133
+ *
134
+ * Requires `@upstash/redis` and `@upstash/realtime` as peer dependencies.
135
+ * Events are stored in Redis Streams and flushed to Postgres on demand.
136
+ * Live deltas are published to `ab:live:{testId}` channels for realtime dashboards.
137
+ */ function upstashAnalytics(options) {
138
+ let db;
139
+ let redis;
140
+ const adapter = {
141
+ tables: {
142
+ abTestAggregations: aggregationsTable
143
+ },
144
+ realtimeInstance: null,
145
+ async init (instance) {
146
+ db = instance;
147
+ const upstashRedis = await _importUpstashRedis();
148
+ redis = new upstashRedis.Redis({
149
+ url: options.url,
150
+ token: options.token
151
+ });
152
+ try {
153
+ const upstashRealtime = await _importUpstashRealtime();
154
+ adapter.realtimeInstance = new upstashRealtime.Realtime({
155
+ url: options.url,
156
+ token: options.token
157
+ });
158
+ } catch {
159
+ // @upstash/realtime not installed -- realtime disabled
160
+ }
161
+ },
162
+ async track (event) {
163
+ // The Upstash adapter is the A/B-dashboard sink: it streams per-test
164
+ // events (keyed by testId) for live deltas + flush-to-aggregations, and
165
+ // it does NOT provision an ab_test_events table. A non-A/B analytics
166
+ // event (no `ab`) therefore has no durable home in this adapter and is
167
+ // DROPPED — there is no other sink to catch it until the M3 event-bus
168
+ // ships (see AB_MEASUREMENT_DESIGN §9 carry-forward). Make the drop loud
169
+ // rather than silent so a single-sink upstash deployment can see it.
170
+ if (!event.ab) {
171
+ console.warn(`[cms] upstashAnalytics dropped a non-A/B event ("${event.name}"): this A/B-realtime adapter has no durable store for non-A/B events. Use the postgres adapter (or wait for the M3 event-bus) to persist page_view / form_submit events.`);
172
+ return;
173
+ }
174
+ const { testId, variantId } = event.ab;
175
+ const streamKey = `ab:events:${testId}`;
176
+ const entry = {
177
+ testId,
178
+ variantId,
179
+ visitorId: event.visitorId ?? '',
180
+ anonymous: String(event.anonymous),
181
+ eventType: event.name,
182
+ timestamp: event.timestamp.toISOString()
183
+ };
184
+ if (event.metadata) {
185
+ entry.metadata = JSON.stringify(event.metadata);
186
+ }
187
+ await redis.xadd(streamKey, '*', entry);
188
+ if (adapter.realtimeInstance) {
189
+ const delta = {
190
+ variantId,
191
+ eventType: event.name,
192
+ count: 1,
193
+ timestamp: Date.now()
194
+ };
195
+ try {
196
+ await redis.publish(`ab:live:${testId}`, JSON.stringify(delta));
197
+ } catch {
198
+ // Non-critical
199
+ }
200
+ }
201
+ },
202
+ async query (testId, options) {
203
+ const fromClause = options?.from ? sql` AND a.period_start >= ${options.from}` : sql``;
204
+ const toClause = options?.to ? sql` AND a.period_end <= ${options.to}` : sql``;
205
+ const rows = await db.execute(sql`
206
+ SELECT
207
+ a.variant_id,
208
+ v.name AS variant_name,
209
+ a.event_type,
210
+ SUM(a.count)::int AS count,
211
+ SUM(a.unique_visitors)::int AS unique_visitors
212
+ FROM cms.ab_test_aggregations a
213
+ INNER JOIN cms.ab_test_variants v ON v.id = a.variant_id
214
+ WHERE a.test_id = ${testId}
215
+ ${fromClause}
216
+ ${toClause}
217
+ GROUP BY a.variant_id, v.name, a.event_type
218
+ ORDER BY a.variant_id, a.event_type
219
+ `);
220
+ const variantMap = new Map();
221
+ for (const row of rows.rows){
222
+ let v = variantMap.get(row.variant_id);
223
+ if (!v) {
224
+ v = {
225
+ variantId: row.variant_id,
226
+ variantName: row.variant_name,
227
+ impressions: 0,
228
+ conversions: 0,
229
+ uniqueVisitors: 0,
230
+ conversionRate: 0,
231
+ // The upstash pre-aggregate flush does not track interaction ids, so
232
+ // the funnel (attempts/completionRate) is the postgres path only.
233
+ attempts: 0,
234
+ completionRate: 0,
235
+ eventBreakdown: {}
236
+ };
237
+ variantMap.set(row.variant_id, v);
238
+ }
239
+ v.eventBreakdown[row.event_type] = {
240
+ count: row.count,
241
+ uniqueVisitors: row.unique_visitors,
242
+ distinctInteractions: 0
243
+ };
244
+ if (row.event_type === 'impression') {
245
+ v.impressions = row.count;
246
+ v.uniqueVisitors = row.unique_visitors;
247
+ } else if (row.event_type === 'conversion') {
248
+ v.conversions = row.count;
249
+ }
250
+ }
251
+ const variants = [
252
+ ...variantMap.values()
253
+ ];
254
+ for (const v of variants){
255
+ v.conversionRate = v.impressions > 0 ? Math.round(v.conversions / v.impressions * 10000) / 100 : 0;
256
+ }
257
+ return {
258
+ testId,
259
+ variants,
260
+ totalImpressions: variants.reduce((s, v)=>s + v.impressions, 0),
261
+ totalConversions: variants.reduce((s, v)=>s + v.conversions, 0)
262
+ };
263
+ },
264
+ async flush (testId) {
265
+ const streamKeys = [];
266
+ if (testId) {
267
+ streamKeys.push(`ab:events:${testId}`);
268
+ } else {
269
+ let cursor = '0';
270
+ do {
271
+ const [nextCursor, keys] = await redis.scan(cursor, {
272
+ match: 'ab:events:*',
273
+ count: 100
274
+ });
275
+ cursor = nextCursor;
276
+ streamKeys.push(...keys);
277
+ }while (cursor !== '0')
278
+ }
279
+ let totalFlushed = 0;
280
+ for (const streamKey of streamKeys){
281
+ const cursorKey = `ab:cursor:${streamKey}`;
282
+ const lastId = await redis.get(cursorKey) ?? '0-0';
283
+ const results = await redis.xread([
284
+ {
285
+ key: streamKey,
286
+ id: lastId
287
+ }
288
+ ], {
289
+ count: 10000
290
+ });
291
+ if (!results || results.length === 0) continue;
292
+ const stream = results[0];
293
+ if (!stream || !stream.messages || stream.messages.length === 0) continue;
294
+ const agg = new Map();
295
+ let maxId = lastId;
296
+ for (const msg of stream.messages){
297
+ maxId = msg.id;
298
+ const d = msg.message;
299
+ const key = `${d.testId}:${d.variantId}:${d.eventType}`;
300
+ let bucket = agg.get(key);
301
+ if (!bucket) {
302
+ bucket = {
303
+ count: 0,
304
+ visitors: new Set()
305
+ };
306
+ agg.set(key, bucket);
307
+ }
308
+ bucket.count++;
309
+ bucket.visitors.add(d.visitorId);
310
+ }
311
+ const now = new Date();
312
+ const periodStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
313
+ const periodEnd = new Date(periodStart.getTime() + 86400000);
314
+ for (const [key, bucket] of agg){
315
+ const [tId, variantId, eventType] = key.split(':');
316
+ const id = newId('abTestAgg');
317
+ await db.execute(sql`
318
+ INSERT INTO cms.ab_test_aggregations
319
+ (id, test_id, variant_id, event_type, count, unique_visitors, period_start, period_end, updated_at)
320
+ VALUES
321
+ (${id}, ${tId}, ${variantId}, ${eventType}, ${bucket.count}, ${bucket.visitors.size}, ${periodStart}, ${periodEnd}, NOW())
322
+ ON CONFLICT (test_id, variant_id, event_type, period_start) DO UPDATE SET
323
+ count = cms.ab_test_aggregations.count + EXCLUDED.count,
324
+ unique_visitors = GREATEST(cms.ab_test_aggregations.unique_visitors, EXCLUDED.unique_visitors),
325
+ updated_at = NOW()
326
+ `);
327
+ totalFlushed += bucket.count;
328
+ }
329
+ await redis.set(cursorKey, maxId);
330
+ await redis.xtrim(streamKey, {
331
+ strategy: 'MINID',
332
+ threshold: maxId
333
+ });
334
+ }
335
+ return {
336
+ flushed: totalFlushed
337
+ };
338
+ }
339
+ };
340
+ return adapter;
341
+ }
342
+
343
+ export { upstashAnalytics };