@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.
- package/README.md +169 -0
- package/dist/ab-edge/index.cjs +214 -0
- package/dist/ab-edge/index.d.cts +121 -0
- package/dist/ab-edge/index.d.ts +121 -0
- package/dist/ab-edge/index.js +205 -0
- package/dist/bin/createcms.js +3082 -0
- package/dist/db.cjs +496 -0
- package/dist/db.d.cts +128 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.js +488 -0
- package/dist/index.cjs +13789 -0
- package/dist/index.d.cts +10277 -0
- package/dist/index.d.ts +10277 -0
- package/dist/index.js +13737 -0
- package/dist/nanoid.cjs +50 -0
- package/dist/nanoid.d.cts +29 -0
- package/dist/nanoid.d.ts +29 -0
- package/dist/nanoid.js +47 -0
- package/dist/next/index.cjs +60 -0
- package/dist/next/index.d.cts +141 -0
- package/dist/next/index.d.ts +141 -0
- package/dist/next/index.js +58 -0
- package/dist/next/middleware.cjs +113 -0
- package/dist/next/middleware.d.cts +77 -0
- package/dist/next/middleware.d.ts +77 -0
- package/dist/next/middleware.js +111 -0
- package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
- package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.js +343 -0
- package/dist/plugins/ab-test/client.cjs +686 -0
- package/dist/plugins/ab-test/client.d.cts +233 -0
- package/dist/plugins/ab-test/client.d.ts +233 -0
- package/dist/plugins/ab-test/client.js +684 -0
- package/dist/plugins/ab-test/index.cjs +3400 -0
- package/dist/plugins/ab-test/index.d.cts +1131 -0
- package/dist/plugins/ab-test/index.d.ts +1131 -0
- package/dist/plugins/ab-test/index.js +3367 -0
- package/dist/plugins/client.cjs +20 -0
- package/dist/plugins/client.d.cts +3 -0
- package/dist/plugins/client.d.ts +3 -0
- package/dist/plugins/client.js +3 -0
- package/dist/plugins/consent/client.cjs +315 -0
- package/dist/plugins/consent/client.d.cts +145 -0
- package/dist/plugins/consent/client.d.ts +145 -0
- package/dist/plugins/consent/client.js +313 -0
- package/dist/plugins/consent/index.cjs +267 -0
- package/dist/plugins/consent/index.d.cts +618 -0
- package/dist/plugins/consent/index.d.ts +618 -0
- package/dist/plugins/consent/index.js +258 -0
- package/dist/plugins/i18n/index.cjs +2177 -0
- package/dist/plugins/i18n/index.d.cts +562 -0
- package/dist/plugins/i18n/index.d.ts +562 -0
- package/dist/plugins/i18n/index.js +2150 -0
- package/dist/plugins/media-optimize/index.cjs +315 -0
- package/dist/plugins/media-optimize/index.d.cts +144 -0
- package/dist/plugins/media-optimize/index.d.ts +144 -0
- package/dist/plugins/media-optimize/index.js +311 -0
- package/dist/plugins/multi-tenant/index.cjs +210 -0
- package/dist/plugins/multi-tenant/index.d.cts +431 -0
- package/dist/plugins/multi-tenant/index.d.ts +431 -0
- package/dist/plugins/multi-tenant/index.js +207 -0
- package/dist/plugins/server.cjs +24 -0
- package/dist/plugins/server.d.cts +3 -0
- package/dist/plugins/server.d.ts +3 -0
- package/dist/plugins/server.js +3 -0
- package/dist/react/blocks.cjs +233 -0
- package/dist/react/blocks.d.cts +320 -0
- package/dist/react/blocks.d.ts +320 -0
- package/dist/react/blocks.js +226 -0
- package/dist/react/index.cjs +901 -0
- package/dist/react/index.d.cts +992 -0
- package/dist/react/index.d.ts +992 -0
- package/dist/react/index.js +872 -0
- package/dist/react/tracking.cjs +243 -0
- package/dist/react/tracking.d.cts +364 -0
- package/dist/react/tracking.d.ts +364 -0
- package/dist/react/tracking.js +216 -0
- package/dist/react/variant.cjs +59 -0
- package/dist/react/variant.d.cts +26 -0
- package/dist/react/variant.d.ts +26 -0
- package/dist/react/variant.js +57 -0
- 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 };
|