@reactionary/core 0.0.29 → 0.0.31
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/index.js +247 -209
- package/package.json +2 -2
- package/src/cache/cache-evaluation.interface.d.ts +17 -0
- package/src/cache/cache.interface.d.ts +36 -0
- package/src/cache/noop-cache.d.ts +18 -0
- package/src/cache/redis-cache.d.ts +13 -9
- package/src/client/client.d.ts +6 -1
- package/src/index.d.ts +3 -1
- package/src/providers/analytics.provider.d.ts +3 -0
- package/src/providers/base.provider.d.ts +13 -2
- package/src/providers/cart.provider.d.ts +3 -0
- package/src/providers/identity.provider.d.ts +3 -0
- package/src/providers/inventory.provider.d.ts +3 -0
- package/src/providers/price.provider.d.ts +3 -0
- package/src/providers/product.provider.d.ts +3 -0
- package/src/providers/search.provider.d.ts +3 -0
- package/src/cache/caching-strategy.d.ts +0 -13
package/index.js
CHANGED
|
@@ -1,47 +1,95 @@
|
|
|
1
|
-
// core/src/cache/caching-strategy.ts
|
|
2
|
-
var BaseCachingStrategy = class {
|
|
3
|
-
get(query, session) {
|
|
4
|
-
const q = query;
|
|
5
|
-
return {
|
|
6
|
-
key: q.sku,
|
|
7
|
-
cacheDurationInSeconds: 300,
|
|
8
|
-
canCache: true
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
1
|
// core/src/cache/redis-cache.ts
|
|
14
2
|
import { Redis } from "@upstash/redis";
|
|
15
3
|
var RedisCache = class {
|
|
16
|
-
constructor(
|
|
17
|
-
this.strategy = strategy;
|
|
4
|
+
constructor() {
|
|
18
5
|
this.redis = Redis.fromEnv();
|
|
19
6
|
}
|
|
20
|
-
async get(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
async get(key, schema) {
|
|
8
|
+
if (!key) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const unvalidated = await this.redis.get(key);
|
|
12
|
+
const parsed = schema.safeParse(unvalidated);
|
|
13
|
+
if (parsed.success) {
|
|
14
|
+
return parsed.data;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
async put(key, value, ttlSeconds) {
|
|
19
|
+
if (!key) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const options = ttlSeconds ? { ex: ttlSeconds } : void 0;
|
|
23
|
+
await this.redis.set(key, value, options);
|
|
24
|
+
}
|
|
25
|
+
async del(keys) {
|
|
26
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
27
|
+
for (const key of keyArray) {
|
|
28
|
+
if (key.includes("*")) {
|
|
29
|
+
const matchingKeys = await this.redis.keys(key);
|
|
30
|
+
if (matchingKeys.length > 0) {
|
|
31
|
+
await this.redis.del(...matchingKeys);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
await this.redis.del(key);
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
|
-
return result;
|
|
31
37
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
async keys(pattern) {
|
|
39
|
+
return await this.redis.keys(pattern);
|
|
40
|
+
}
|
|
41
|
+
async clear(pattern) {
|
|
42
|
+
const searchPattern = pattern || "*";
|
|
43
|
+
const keys = await this.redis.keys(searchPattern);
|
|
44
|
+
if (keys.length > 0) {
|
|
45
|
+
await this.redis.del(...keys);
|
|
36
46
|
}
|
|
37
47
|
}
|
|
48
|
+
async getStats() {
|
|
49
|
+
const keys = await this.redis.keys("*");
|
|
50
|
+
return {
|
|
51
|
+
hits: 0,
|
|
52
|
+
// Would need to track this separately
|
|
53
|
+
misses: 0,
|
|
54
|
+
// Would need to track this separately
|
|
55
|
+
size: keys.length
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// core/src/cache/noop-cache.ts
|
|
61
|
+
var NoOpCache = class {
|
|
62
|
+
async get(_key, _schema) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
async put(_key, _value, _ttlSeconds) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
async del(_keys) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
async keys(_pattern) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
async clear(_pattern) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
async getStats() {
|
|
78
|
+
return {
|
|
79
|
+
hits: 0,
|
|
80
|
+
misses: 0,
|
|
81
|
+
size: 0
|
|
82
|
+
};
|
|
83
|
+
}
|
|
38
84
|
};
|
|
39
85
|
|
|
40
86
|
// core/src/client/client.ts
|
|
41
|
-
function buildClient(
|
|
87
|
+
function buildClient(providerFactories, options = {}) {
|
|
42
88
|
let client = {};
|
|
89
|
+
const sharedCache = options.cache || new RedisCache();
|
|
43
90
|
const mergedAnalytics = [];
|
|
44
|
-
for (const
|
|
91
|
+
for (const factory of providerFactories) {
|
|
92
|
+
const provider = factory(sharedCache);
|
|
45
93
|
client = {
|
|
46
94
|
...client,
|
|
47
95
|
...provider
|
|
@@ -51,184 +99,48 @@ function buildClient(providers) {
|
|
|
51
99
|
}
|
|
52
100
|
}
|
|
53
101
|
client.analytics = mergedAnalytics;
|
|
54
|
-
|
|
102
|
+
const completeClient = {
|
|
103
|
+
...client,
|
|
104
|
+
cache: sharedCache
|
|
105
|
+
};
|
|
106
|
+
return completeClient;
|
|
107
|
+
}
|
|
108
|
+
function createCache() {
|
|
109
|
+
return new RedisCache();
|
|
55
110
|
}
|
|
56
111
|
|
|
57
|
-
// otel/src/
|
|
58
|
-
import {
|
|
59
|
-
import { resourceFromAttributes, defaultResource } from "@opentelemetry/resources";
|
|
60
|
-
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION, SEMRESATTRS_DEPLOYMENT_ENVIRONMENT } from "@opentelemetry/semantic-conventions";
|
|
112
|
+
// otel/src/trpc-middleware.ts
|
|
113
|
+
import { TRPCError } from "@trpc/server";
|
|
61
114
|
import {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
|
|
115
|
+
SpanKind as SpanKind2,
|
|
116
|
+
SpanStatusCode as SpanStatusCode3
|
|
117
|
+
} from "@opentelemetry/api";
|
|
66
118
|
|
|
67
|
-
// otel/src/
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return typeof window !== "undefined" && typeof process === "undefined";
|
|
74
|
-
}
|
|
75
|
-
function getConfigFromEnv() {
|
|
76
|
-
if (isBrowser()) {
|
|
77
|
-
return {
|
|
78
|
-
serviceName: "browser-service",
|
|
79
|
-
serviceVersion: void 0,
|
|
80
|
-
environment: "browser",
|
|
81
|
-
otlpEndpoint: void 0,
|
|
82
|
-
otlpHeaders: void 0,
|
|
83
|
-
traceEnabled: false,
|
|
84
|
-
metricsEnabled: false,
|
|
85
|
-
metricExportIntervalMillis: 6e4
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
const tracesExporter = process.env["OTEL_TRACES_EXPORTER"] || "console";
|
|
89
|
-
const metricsExporter = process.env["OTEL_METRICS_EXPORTER"] || "console";
|
|
90
|
-
return {
|
|
91
|
-
serviceName: process.env["OTEL_SERVICE_NAME"] || "unknown_service",
|
|
92
|
-
serviceVersion: process.env["OTEL_SERVICE_VERSION"],
|
|
93
|
-
environment: process.env["DEPLOYMENT_ENVIRONMENT"] || process.env["NODE_ENV"] || "development",
|
|
94
|
-
otlpEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
|
|
95
|
-
otlpHeaders: process.env["OTEL_EXPORTER_OTLP_HEADERS"] ? parseHeaders(process.env["OTEL_EXPORTER_OTLP_HEADERS"]) : void 0,
|
|
96
|
-
traceEnabled: tracesExporter !== "none",
|
|
97
|
-
metricsEnabled: metricsExporter !== "none",
|
|
98
|
-
metricExportIntervalMillis: process.env["OTEL_METRIC_EXPORT_INTERVAL"] ? parseInt(process.env["OTEL_METRIC_EXPORT_INTERVAL"], 10) : 6e4
|
|
99
|
-
// Default 60 seconds per OTEL spec
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function parseHeaders(headerString) {
|
|
103
|
-
const headers = {};
|
|
104
|
-
headerString.split(",").forEach((header) => {
|
|
105
|
-
const [key, value] = header.split("=");
|
|
106
|
-
if (key && value) {
|
|
107
|
-
headers[key.trim()] = value.trim();
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
return headers;
|
|
111
|
-
}
|
|
112
|
-
function createTraceExporter(config) {
|
|
113
|
-
if (!config.traceEnabled) {
|
|
114
|
-
return void 0;
|
|
115
|
-
}
|
|
116
|
-
const tracesExporter = process.env["OTEL_TRACES_EXPORTER"] || "console";
|
|
117
|
-
switch (tracesExporter) {
|
|
118
|
-
case "otlp":
|
|
119
|
-
case "otlp/http": {
|
|
120
|
-
if (!config.otlpEndpoint) {
|
|
121
|
-
console.warn("OTEL_EXPORTER_OTLP_ENDPOINT not set, falling back to console");
|
|
122
|
-
return new ConsoleSpanExporter();
|
|
123
|
-
}
|
|
124
|
-
const tracesEndpoint = process.env["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] || `${config.otlpEndpoint}/v1/traces`;
|
|
125
|
-
return new OTLPTraceExporter({
|
|
126
|
-
url: tracesEndpoint,
|
|
127
|
-
headers: config.otlpHeaders
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
case "console":
|
|
131
|
-
return new ConsoleSpanExporter();
|
|
132
|
-
case "none":
|
|
133
|
-
default:
|
|
134
|
-
return void 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function createMetricReader(config) {
|
|
138
|
-
if (!config.metricsEnabled) {
|
|
139
|
-
return void 0;
|
|
140
|
-
}
|
|
141
|
-
const metricsExporter = process.env["OTEL_METRICS_EXPORTER"] || "console";
|
|
142
|
-
let exporter;
|
|
143
|
-
switch (metricsExporter) {
|
|
144
|
-
case "otlp":
|
|
145
|
-
case "otlp/http": {
|
|
146
|
-
if (!config.otlpEndpoint) {
|
|
147
|
-
console.warn("OTEL_EXPORTER_OTLP_ENDPOINT not set, falling back to console");
|
|
148
|
-
exporter = new ConsoleMetricExporter();
|
|
149
|
-
} else {
|
|
150
|
-
const metricsEndpoint = process.env["OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"] || `${config.otlpEndpoint}/v1/metrics`;
|
|
151
|
-
exporter = new OTLPMetricExporter({
|
|
152
|
-
url: metricsEndpoint,
|
|
153
|
-
headers: config.otlpHeaders
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
case "console":
|
|
159
|
-
exporter = new ConsoleMetricExporter();
|
|
160
|
-
break;
|
|
161
|
-
case "none":
|
|
162
|
-
default:
|
|
163
|
-
return void 0;
|
|
164
|
-
}
|
|
165
|
-
return new PeriodicExportingMetricReader({
|
|
166
|
-
exporter,
|
|
167
|
-
exportIntervalMillis: config.metricExportIntervalMillis
|
|
168
|
-
});
|
|
169
|
-
}
|
|
119
|
+
// otel/src/tracer.ts
|
|
120
|
+
import {
|
|
121
|
+
trace,
|
|
122
|
+
SpanStatusCode,
|
|
123
|
+
context as otelContext
|
|
124
|
+
} from "@opentelemetry/api";
|
|
170
125
|
|
|
171
126
|
// otel/src/sdk.ts
|
|
127
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
172
128
|
var sdk = null;
|
|
173
129
|
var isInitialized = false;
|
|
174
130
|
var initializationPromise = null;
|
|
175
|
-
function
|
|
131
|
+
function isBrowser() {
|
|
176
132
|
return typeof window !== "undefined" && typeof process === "undefined";
|
|
177
133
|
}
|
|
178
134
|
function ensureInitialized() {
|
|
179
135
|
if (isInitialized || initializationPromise) {
|
|
180
136
|
return;
|
|
181
137
|
}
|
|
182
|
-
if (
|
|
138
|
+
if (isBrowser()) {
|
|
183
139
|
isInitialized = true;
|
|
184
140
|
return;
|
|
185
141
|
}
|
|
186
142
|
initializationPromise = Promise.resolve().then(() => {
|
|
187
|
-
|
|
188
|
-
if (process.env["OTEL_LOG_LEVEL"] === "debug") {
|
|
189
|
-
console.log("OpenTelemetry auto-initializing with config:", {
|
|
190
|
-
serviceName: config.serviceName,
|
|
191
|
-
environment: config.environment,
|
|
192
|
-
tracesExporter: process.env["OTEL_TRACES_EXPORTER"] || "console",
|
|
193
|
-
metricsExporter: process.env["OTEL_METRICS_EXPORTER"] || "console"
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
const attributes = {
|
|
197
|
-
[SEMRESATTRS_SERVICE_NAME]: config.serviceName
|
|
198
|
-
};
|
|
199
|
-
if (config.serviceVersion) {
|
|
200
|
-
attributes[SEMRESATTRS_SERVICE_VERSION] = config.serviceVersion;
|
|
201
|
-
}
|
|
202
|
-
if (config.environment) {
|
|
203
|
-
attributes[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT] = config.environment;
|
|
204
|
-
}
|
|
205
|
-
const customResource = resourceFromAttributes(attributes);
|
|
206
|
-
const resource = defaultResource().merge(customResource);
|
|
207
|
-
const traceExporter = createTraceExporter(config);
|
|
208
|
-
const metricReader = createMetricReader(config);
|
|
209
|
-
const instrumentations = [
|
|
210
|
-
new HttpInstrumentation({
|
|
211
|
-
requestHook: (span, request) => {
|
|
212
|
-
const req = request;
|
|
213
|
-
if (req.headers) {
|
|
214
|
-
span.setAttribute("http.request.body.size", req.headers["content-length"] || 0);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
responseHook: (span, response) => {
|
|
218
|
-
const res = response;
|
|
219
|
-
if (res.headers) {
|
|
220
|
-
span.setAttribute("http.response.body.size", res.headers["content-length"] || 0);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}),
|
|
224
|
-
new ExpressInstrumentation()
|
|
225
|
-
];
|
|
226
|
-
sdk = new NodeSDK({
|
|
227
|
-
resource,
|
|
228
|
-
spanProcessors: traceExporter ? [new BatchSpanProcessor(traceExporter)] : [],
|
|
229
|
-
metricReader,
|
|
230
|
-
instrumentations
|
|
231
|
-
});
|
|
143
|
+
sdk = new NodeSDK();
|
|
232
144
|
sdk.start();
|
|
233
145
|
isInitialized = true;
|
|
234
146
|
process.on("SIGTERM", async () => {
|
|
@@ -256,11 +168,6 @@ function isOtelInitialized() {
|
|
|
256
168
|
}
|
|
257
169
|
|
|
258
170
|
// otel/src/tracer.ts
|
|
259
|
-
import {
|
|
260
|
-
trace,
|
|
261
|
-
SpanStatusCode,
|
|
262
|
-
context as otelContext
|
|
263
|
-
} from "@opentelemetry/api";
|
|
264
171
|
import { SpanKind, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
|
|
265
172
|
var TRACER_NAME = "@reactionary/otel";
|
|
266
173
|
var TRACER_VERSION = "0.0.1";
|
|
@@ -356,13 +263,6 @@ function getMetrics() {
|
|
|
356
263
|
return metricsInstance;
|
|
357
264
|
}
|
|
358
265
|
|
|
359
|
-
// otel/src/trpc-middleware.ts
|
|
360
|
-
import { TRPCError } from "@trpc/server";
|
|
361
|
-
import {
|
|
362
|
-
SpanKind as SpanKind2,
|
|
363
|
-
SpanStatusCode as SpanStatusCode3
|
|
364
|
-
} from "@opentelemetry/api";
|
|
365
|
-
|
|
366
266
|
// otel/src/provider-instrumentation.ts
|
|
367
267
|
import { SpanKind as SpanKind3 } from "@opentelemetry/api";
|
|
368
268
|
async function withProviderSpan(options, fn) {
|
|
@@ -440,16 +340,15 @@ function createProviderInstrumentation(providerName) {
|
|
|
440
340
|
};
|
|
441
341
|
}
|
|
442
342
|
|
|
443
|
-
// otel/src/index.ts
|
|
444
|
-
import { trace as trace2, context, SpanKind as SpanKind4, SpanStatusCode as SpanStatusCode4 } from "@opentelemetry/api";
|
|
445
|
-
|
|
446
343
|
// core/src/providers/base.provider.ts
|
|
344
|
+
import * as crypto from "crypto";
|
|
447
345
|
var BaseProvider = class {
|
|
448
|
-
constructor(schema, querySchema, mutationSchema) {
|
|
346
|
+
constructor(schema, querySchema, mutationSchema, cache) {
|
|
449
347
|
this.schema = schema;
|
|
450
348
|
this.querySchema = querySchema;
|
|
451
349
|
this.mutationSchema = mutationSchema;
|
|
452
350
|
this.instrumentation = createProviderInstrumentation(this.constructor.name);
|
|
351
|
+
this.cache = cache;
|
|
453
352
|
}
|
|
454
353
|
/**
|
|
455
354
|
* Validates that the final domain model constructed by the provider
|
|
@@ -475,11 +374,44 @@ var BaseProvider = class {
|
|
|
475
374
|
"query",
|
|
476
375
|
async (span) => {
|
|
477
376
|
span.setAttribute("provider.query.count", queries.length);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
377
|
+
let cacheHits = 0;
|
|
378
|
+
let cacheMisses = 0;
|
|
379
|
+
const results = [];
|
|
380
|
+
for (const query of queries) {
|
|
381
|
+
let result = null;
|
|
382
|
+
const cacheInfo = this.getCacheEvaluation(query, session);
|
|
383
|
+
if (cacheInfo.canCache && cacheInfo.key) {
|
|
384
|
+
try {
|
|
385
|
+
result = await this.cache.get(cacheInfo.key, this.schema);
|
|
386
|
+
if (result) {
|
|
387
|
+
cacheHits++;
|
|
388
|
+
span.setAttribute("provider.cache.hit", true);
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.warn(`Cache get error for ${this.constructor.name}:`, error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!result) {
|
|
395
|
+
const singleResult = await this.fetch([query], session);
|
|
396
|
+
result = singleResult[0];
|
|
397
|
+
cacheMisses++;
|
|
398
|
+
if (result && cacheInfo.canCache && cacheInfo.key) {
|
|
399
|
+
try {
|
|
400
|
+
await this.cache.put(cacheInfo.key, result, cacheInfo.cacheDurationInSeconds);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn(`Cache put error for ${this.constructor.name}:`, error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (result) {
|
|
407
|
+
this.assert(result);
|
|
408
|
+
results.push(result);
|
|
409
|
+
}
|
|
481
410
|
}
|
|
482
411
|
span.setAttribute("provider.result.count", results.length);
|
|
412
|
+
span.setAttribute("provider.cache.hits", cacheHits);
|
|
413
|
+
span.setAttribute("provider.cache.misses", cacheMisses);
|
|
414
|
+
span.setAttribute("provider.cache.hit_ratio", cacheHits / (cacheHits + cacheMisses));
|
|
483
415
|
return results;
|
|
484
416
|
},
|
|
485
417
|
{ queryCount: queries.length }
|
|
@@ -501,34 +433,139 @@ var BaseProvider = class {
|
|
|
501
433
|
{ mutationCount: mutations.length }
|
|
502
434
|
);
|
|
503
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Provider-specific cache evaluation logic.
|
|
438
|
+
* Returns information about how this query should be cached.
|
|
439
|
+
* Override this method to enable caching with custom keys and TTL.
|
|
440
|
+
* Default implementation returns no caching.
|
|
441
|
+
*/
|
|
442
|
+
getCacheEvaluation(query, session) {
|
|
443
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
444
|
+
const userId = session.identity?.id || "anonymous";
|
|
445
|
+
const queryHash = crypto.createHash("md5").update(JSON.stringify(query)).digest("hex").substring(0, 12);
|
|
446
|
+
const key = `${providerName}:${userId}:${queryHash}`;
|
|
447
|
+
return {
|
|
448
|
+
key,
|
|
449
|
+
cacheDurationInSeconds: 0,
|
|
450
|
+
canCache: false
|
|
451
|
+
};
|
|
452
|
+
}
|
|
504
453
|
};
|
|
505
454
|
|
|
506
455
|
// core/src/providers/analytics.provider.ts
|
|
456
|
+
import * as crypto2 from "crypto";
|
|
507
457
|
var AnalyticsProvider = class extends BaseProvider {
|
|
458
|
+
getCacheEvaluation(query, _session) {
|
|
459
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
460
|
+
const relevantFields = {
|
|
461
|
+
type: typeof query === "object" && query !== null && "type" in query ? query.type : void 0,
|
|
462
|
+
dateRange: typeof query === "object" && query !== null && "dateRange" in query ? query.dateRange : void 0,
|
|
463
|
+
filters: typeof query === "object" && query !== null && "filters" in query ? query.filters : void 0
|
|
464
|
+
};
|
|
465
|
+
const analyticsHash = crypto2.createHash("md5").update(JSON.stringify(relevantFields)).digest("hex").substring(0, 12);
|
|
466
|
+
const key = `${providerName}:analytics:${analyticsHash}`;
|
|
467
|
+
return {
|
|
468
|
+
key,
|
|
469
|
+
cacheDurationInSeconds: 0,
|
|
470
|
+
canCache: false
|
|
471
|
+
};
|
|
472
|
+
}
|
|
508
473
|
};
|
|
509
474
|
|
|
510
475
|
// core/src/providers/cart.provider.ts
|
|
476
|
+
import * as crypto3 from "crypto";
|
|
511
477
|
var CartProvider = class extends BaseProvider {
|
|
478
|
+
getCacheEvaluation(query, session) {
|
|
479
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
480
|
+
const userId = session.identity?.id || "anonymous";
|
|
481
|
+
const queryHash = crypto3.createHash("md5").update(JSON.stringify(query)).digest("hex").substring(0, 12);
|
|
482
|
+
const key = `${providerName}:cart:${userId}:${queryHash}`;
|
|
483
|
+
return {
|
|
484
|
+
key,
|
|
485
|
+
cacheDurationInSeconds: 0,
|
|
486
|
+
canCache: false
|
|
487
|
+
};
|
|
488
|
+
}
|
|
512
489
|
};
|
|
513
490
|
|
|
514
491
|
// core/src/providers/identity.provider.ts
|
|
515
492
|
var IdentityProvider = class extends BaseProvider {
|
|
493
|
+
getCacheEvaluation(_query, session) {
|
|
494
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
495
|
+
const userId = session.identity?.id;
|
|
496
|
+
const key = userId ? `${providerName}:identity:${userId}` : `${providerName}:identity:anonymous`;
|
|
497
|
+
return {
|
|
498
|
+
key,
|
|
499
|
+
cacheDurationInSeconds: 0,
|
|
500
|
+
canCache: false
|
|
501
|
+
};
|
|
502
|
+
}
|
|
516
503
|
};
|
|
517
504
|
|
|
518
505
|
// core/src/providers/inventory.provider.ts
|
|
519
506
|
var InventoryProvider = class extends BaseProvider {
|
|
507
|
+
getCacheEvaluation(query, _session) {
|
|
508
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
509
|
+
const key = `${providerName}:inventory:${query.sku}`;
|
|
510
|
+
return {
|
|
511
|
+
key,
|
|
512
|
+
cacheDurationInSeconds: 0,
|
|
513
|
+
canCache: false
|
|
514
|
+
};
|
|
515
|
+
}
|
|
520
516
|
};
|
|
521
517
|
|
|
522
518
|
// core/src/providers/price.provider.ts
|
|
519
|
+
import * as crypto4 from "crypto";
|
|
523
520
|
var PriceProvider = class extends BaseProvider {
|
|
521
|
+
getCacheEvaluation(query, _session) {
|
|
522
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
523
|
+
const skuHash = crypto4.createHash("md5").update(JSON.stringify(query.sku)).digest("hex").substring(0, 12);
|
|
524
|
+
const key = `${providerName}:price:${skuHash}`;
|
|
525
|
+
return {
|
|
526
|
+
key,
|
|
527
|
+
cacheDurationInSeconds: 0,
|
|
528
|
+
canCache: false
|
|
529
|
+
};
|
|
530
|
+
}
|
|
524
531
|
};
|
|
525
532
|
|
|
526
533
|
// core/src/providers/product.provider.ts
|
|
534
|
+
import * as crypto5 from "crypto";
|
|
527
535
|
var ProductProvider = class extends BaseProvider {
|
|
536
|
+
getCacheEvaluation(query, _session) {
|
|
537
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
538
|
+
let key;
|
|
539
|
+
if (query.query === "slug") {
|
|
540
|
+
key = `${providerName}:product:slug:${query.slug}`;
|
|
541
|
+
} else if (query.query === "id") {
|
|
542
|
+
key = `${providerName}:product:id:${query.id}`;
|
|
543
|
+
} else {
|
|
544
|
+
const queryHash = crypto5.createHash("md5").update(JSON.stringify(query)).digest("hex").substring(0, 12);
|
|
545
|
+
key = `${providerName}:product:${queryHash}`;
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
key,
|
|
549
|
+
cacheDurationInSeconds: 300,
|
|
550
|
+
// Products are moderately stable - 5 minutes
|
|
551
|
+
canCache: true
|
|
552
|
+
};
|
|
553
|
+
}
|
|
528
554
|
};
|
|
529
555
|
|
|
530
556
|
// core/src/providers/search.provider.ts
|
|
557
|
+
import * as crypto6 from "crypto";
|
|
531
558
|
var SearchProvider = class extends BaseProvider {
|
|
559
|
+
getCacheEvaluation(query, _session) {
|
|
560
|
+
const providerName = this.constructor.name.toLowerCase();
|
|
561
|
+
const searchHash = crypto6.createHash("md5").update(JSON.stringify(query.search)).digest("hex").substring(0, 12);
|
|
562
|
+
const key = `${providerName}:search:${searchHash}`;
|
|
563
|
+
return {
|
|
564
|
+
key,
|
|
565
|
+
cacheDurationInSeconds: 0,
|
|
566
|
+
canCache: false
|
|
567
|
+
};
|
|
568
|
+
}
|
|
532
569
|
};
|
|
533
570
|
|
|
534
571
|
// core/src/schemas/capabilities.schema.ts
|
|
@@ -986,7 +1023,6 @@ var SearchQueryByTermSchema = BaseQuerySchema.extend({
|
|
|
986
1023
|
var SearchQuerySchema = z25.union([SearchQueryByTermSchema]);
|
|
987
1024
|
export {
|
|
988
1025
|
AnalyticsProvider,
|
|
989
|
-
BaseCachingStrategy,
|
|
990
1026
|
BaseModelSchema,
|
|
991
1027
|
BaseMutationSchema,
|
|
992
1028
|
BaseProvider,
|
|
@@ -1021,6 +1057,7 @@ export {
|
|
|
1021
1057
|
InventorySchema,
|
|
1022
1058
|
MetaSchema,
|
|
1023
1059
|
MonetaryAmountSchema,
|
|
1060
|
+
NoOpCache,
|
|
1024
1061
|
PriceIdentifierSchema,
|
|
1025
1062
|
PriceMutationSchema,
|
|
1026
1063
|
PriceProvider,
|
|
@@ -1048,5 +1085,6 @@ export {
|
|
|
1048
1085
|
SearchResultProductSchema,
|
|
1049
1086
|
SearchResultSchema,
|
|
1050
1087
|
SessionSchema,
|
|
1051
|
-
buildClient
|
|
1088
|
+
buildClient,
|
|
1089
|
+
createCache
|
|
1052
1090
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache evaluation result that determines how and if a query should be cached
|
|
3
|
+
*/
|
|
4
|
+
export interface CacheEvaluation {
|
|
5
|
+
/**
|
|
6
|
+
* The cache key to use for storing/retrieving the value
|
|
7
|
+
*/
|
|
8
|
+
key: string;
|
|
9
|
+
/**
|
|
10
|
+
* How long to cache the value in seconds
|
|
11
|
+
*/
|
|
12
|
+
cacheDurationInSeconds: number;
|
|
13
|
+
/**
|
|
14
|
+
* Whether this query result can be cached
|
|
15
|
+
*/
|
|
16
|
+
canCache: boolean;
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Generic cache interface that can be implemented by different cache backends
|
|
4
|
+
* (Redis, memory, file-based, etc.)
|
|
5
|
+
*/
|
|
6
|
+
export interface Cache {
|
|
7
|
+
/**
|
|
8
|
+
* Retrieves a value from cache and validates it against the provided schema
|
|
9
|
+
*/
|
|
10
|
+
get<T>(key: string, schema: z.ZodType<T>): Promise<T | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Stores a value in cache with optional expiration time
|
|
13
|
+
*/
|
|
14
|
+
put(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Removes one or more keys from cache
|
|
17
|
+
* Supports wildcard patterns (implementation dependent)
|
|
18
|
+
*/
|
|
19
|
+
del(keys: string | string[]): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Finds all keys matching a pattern (implementation dependent)
|
|
22
|
+
*/
|
|
23
|
+
keys(pattern: string): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Clears all cache entries or entries matching a pattern
|
|
26
|
+
*/
|
|
27
|
+
clear(pattern?: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Gets basic cache statistics (implementation dependent)
|
|
30
|
+
*/
|
|
31
|
+
getStats(): Promise<{
|
|
32
|
+
hits: number;
|
|
33
|
+
misses: number;
|
|
34
|
+
size: number;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Cache } from './cache.interface';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* No-op cache implementation that never stores or returns data.
|
|
5
|
+
* Useful for testing or when caching should be disabled.
|
|
6
|
+
*/
|
|
7
|
+
export declare class NoOpCache implements Cache {
|
|
8
|
+
get<T>(_key: string, _schema: z.ZodType<T>): Promise<T | null>;
|
|
9
|
+
put(_key: string, _value: unknown, _ttlSeconds?: number): Promise<void>;
|
|
10
|
+
del(_keys: string | string[]): Promise<void>;
|
|
11
|
+
keys(_pattern: string): Promise<string[]>;
|
|
12
|
+
clear(_pattern?: string): Promise<void>;
|
|
13
|
+
getStats(): Promise<{
|
|
14
|
+
hits: number;
|
|
15
|
+
misses: number;
|
|
16
|
+
size: number;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Redis } from '@upstash/redis';
|
|
2
|
-
import {
|
|
3
|
-
import { BaseQuery } from '../schemas/queries/base.query';
|
|
4
|
-
import { Session } from '../schemas/session.schema';
|
|
5
|
-
import { BaseModel } from '../schemas/models/base.model';
|
|
2
|
+
import { Cache } from './cache.interface';
|
|
6
3
|
import z from 'zod';
|
|
7
|
-
export declare class RedisCache {
|
|
8
|
-
protected strategy: CachingStrategy;
|
|
4
|
+
export declare class RedisCache implements Cache {
|
|
9
5
|
protected redis: Redis;
|
|
10
|
-
constructor(
|
|
11
|
-
get<T
|
|
12
|
-
put(
|
|
6
|
+
constructor();
|
|
7
|
+
get<T>(key: string, schema: z.ZodType<T>): Promise<T | null>;
|
|
8
|
+
put(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
9
|
+
del(keys: string | string[]): Promise<void>;
|
|
10
|
+
keys(pattern: string): Promise<string[]>;
|
|
11
|
+
clear(pattern?: string): Promise<void>;
|
|
12
|
+
getStats(): Promise<{
|
|
13
|
+
hits: number;
|
|
14
|
+
misses: number;
|
|
15
|
+
size: number;
|
|
16
|
+
}>;
|
|
13
17
|
}
|
package/src/client/client.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { IdentityProvider } from '../providers/identity.provider';
|
|
|
5
5
|
import { CartProvider } from "../providers/cart.provider";
|
|
6
6
|
import { PriceProvider } from "../providers/price.provider";
|
|
7
7
|
import { InventoryProvider } from "../providers/inventory.provider";
|
|
8
|
+
import { Cache } from "../cache/cache.interface";
|
|
8
9
|
export interface Client {
|
|
9
10
|
product: ProductProvider;
|
|
10
11
|
search: SearchProvider;
|
|
@@ -15,4 +16,8 @@ export interface Client {
|
|
|
15
16
|
price: PriceProvider;
|
|
16
17
|
inventory: InventoryProvider;
|
|
17
18
|
}
|
|
18
|
-
export
|
|
19
|
+
export interface BuildClientOptions {
|
|
20
|
+
cache?: Cache;
|
|
21
|
+
}
|
|
22
|
+
export declare function buildClient<T extends Partial<Client>>(providerFactories: Array<(cache: Cache) => T>, options?: BuildClientOptions): Required<T>;
|
|
23
|
+
export declare function createCache(): Cache;
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export * from './cache/
|
|
1
|
+
export * from './cache/cache.interface';
|
|
2
|
+
export * from './cache/cache-evaluation.interface';
|
|
2
3
|
export * from './cache/redis-cache';
|
|
4
|
+
export * from './cache/noop-cache';
|
|
3
5
|
export * from './client/client';
|
|
4
6
|
export * from './providers/analytics.provider';
|
|
5
7
|
export * from './providers/base.provider';
|
|
@@ -2,5 +2,8 @@ import { AnalyticsEvent } from '../schemas/models/analytics.model';
|
|
|
2
2
|
import { AnalyticsMutation } from '../schemas/mutations/analytics.mutation';
|
|
3
3
|
import { AnalyticsQuery } from '../schemas/queries/analytics.query';
|
|
4
4
|
import { BaseProvider } from './base.provider';
|
|
5
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
6
|
+
import { Session } from '../schemas/session.schema';
|
|
5
7
|
export declare abstract class AnalyticsProvider<T extends AnalyticsEvent = AnalyticsEvent, Q extends AnalyticsQuery = AnalyticsQuery, M extends AnalyticsMutation = AnalyticsMutation> extends BaseProvider<T, Q, M> {
|
|
8
|
+
protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
|
|
6
9
|
}
|
|
@@ -3,6 +3,9 @@ import { Session } from '../schemas/session.schema';
|
|
|
3
3
|
import { BaseQuery } from '../schemas/queries/base.query';
|
|
4
4
|
import { BaseMutation } from '../schemas/mutations/base.mutation';
|
|
5
5
|
import { BaseModel } from '../schemas/models/base.model';
|
|
6
|
+
import { createProviderInstrumentation } from '@reactionary/otel';
|
|
7
|
+
import { Cache } from '../cache/cache.interface';
|
|
8
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
6
9
|
/**
|
|
7
10
|
* Base capability provider, responsible for mutations (changes) and queries (fetches)
|
|
8
11
|
* for a given business object domain.
|
|
@@ -11,8 +14,9 @@ export declare abstract class BaseProvider<T extends BaseModel = BaseModel, Q ex
|
|
|
11
14
|
readonly schema: z.ZodType<T>;
|
|
12
15
|
readonly querySchema: z.ZodType<Q, Q>;
|
|
13
16
|
readonly mutationSchema: z.ZodType<M, M>;
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
protected instrumentation: ReturnType<typeof createProviderInstrumentation>;
|
|
18
|
+
protected cache: Cache;
|
|
19
|
+
constructor(schema: z.ZodType<T>, querySchema: z.ZodType<Q, Q>, mutationSchema: z.ZodType<M, M>, cache: Cache);
|
|
16
20
|
/**
|
|
17
21
|
* Validates that the final domain model constructed by the provider
|
|
18
22
|
* fulfills the schema as defined. This will throw an exception.
|
|
@@ -44,4 +48,11 @@ export declare abstract class BaseProvider<T extends BaseModel = BaseModel, Q ex
|
|
|
44
48
|
* capabilities.
|
|
45
49
|
*/
|
|
46
50
|
protected abstract process(mutations: M[], session: Session): Promise<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Provider-specific cache evaluation logic.
|
|
53
|
+
* Returns information about how this query should be cached.
|
|
54
|
+
* Override this method to enable caching with custom keys and TTL.
|
|
55
|
+
* Default implementation returns no caching.
|
|
56
|
+
*/
|
|
57
|
+
protected getCacheEvaluation(query: Q, session: Session): CacheEvaluation;
|
|
47
58
|
}
|
|
@@ -2,5 +2,8 @@ import { CartQuery } from "../schemas/queries/cart.query";
|
|
|
2
2
|
import { CartMutation } from "../schemas/mutations/cart.mutation";
|
|
3
3
|
import { BaseProvider } from "./base.provider";
|
|
4
4
|
import { Cart } from "../schemas/models/cart.model";
|
|
5
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
6
|
+
import { Session } from '../schemas/session.schema';
|
|
5
7
|
export declare abstract class CartProvider<T extends Cart = Cart, Q extends CartQuery = CartQuery, M extends CartMutation = CartMutation> extends BaseProvider<T, Q, M> {
|
|
8
|
+
protected getCacheEvaluation(query: Q, session: Session): CacheEvaluation;
|
|
6
9
|
}
|
|
@@ -2,5 +2,8 @@ import { Identity } from "../schemas/models/identity.model";
|
|
|
2
2
|
import { IdentityQuery } from "../schemas/queries/identity.query";
|
|
3
3
|
import { IdentityMutation } from "../schemas/mutations/identity.mutation";
|
|
4
4
|
import { BaseProvider } from "./base.provider";
|
|
5
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
6
|
+
import { Session } from '../schemas/session.schema';
|
|
5
7
|
export declare abstract class IdentityProvider<T extends Identity = Identity, Q extends IdentityQuery = IdentityQuery, M extends IdentityMutation = IdentityMutation> extends BaseProvider<T, Q, M> {
|
|
8
|
+
protected getCacheEvaluation(_query: Q, session: Session): CacheEvaluation;
|
|
6
9
|
}
|
|
@@ -2,5 +2,8 @@ import { Inventory } from '../schemas/models/inventory.model';
|
|
|
2
2
|
import { InventoryQuery } from '../schemas/queries/inventory.query';
|
|
3
3
|
import { InventoryMutation } from '../schemas/mutations/inventory.mutation';
|
|
4
4
|
import { BaseProvider } from './base.provider';
|
|
5
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
6
|
+
import { Session } from '../schemas/session.schema';
|
|
5
7
|
export declare abstract class InventoryProvider<T extends Inventory = Inventory, Q extends InventoryQuery = InventoryQuery, M extends InventoryMutation = InventoryMutation> extends BaseProvider<T, Q, M> {
|
|
8
|
+
protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
|
|
6
9
|
}
|
|
@@ -2,5 +2,8 @@ import { Price } from '../schemas/models/price.model';
|
|
|
2
2
|
import { PriceMutation } from '../schemas/mutations/price.mutation';
|
|
3
3
|
import { PriceQuery } from '../schemas/queries/price.query';
|
|
4
4
|
import { BaseProvider } from './base.provider';
|
|
5
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
6
|
+
import { Session } from '../schemas/session.schema';
|
|
5
7
|
export declare abstract class PriceProvider<T extends Price = Price, Q extends PriceQuery = PriceQuery, M extends PriceMutation = PriceMutation> extends BaseProvider<T, Q, M> {
|
|
8
|
+
protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
|
|
6
9
|
}
|
|
@@ -2,5 +2,8 @@ import { Product } from '../schemas/models/product.model';
|
|
|
2
2
|
import { ProductMutation } from '../schemas/mutations/product.mutation';
|
|
3
3
|
import { ProductQuery } from '../schemas/queries/product.query';
|
|
4
4
|
import { BaseProvider } from './base.provider';
|
|
5
|
+
import { Session } from '../schemas/session.schema';
|
|
6
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
5
7
|
export declare abstract class ProductProvider<T extends Product = Product, Q extends ProductQuery = ProductQuery, M extends ProductMutation = ProductMutation> extends BaseProvider<T, Q, M> {
|
|
8
|
+
protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
|
|
6
9
|
}
|
|
@@ -2,5 +2,8 @@ import { SearchResult } from '../schemas/models/search.model';
|
|
|
2
2
|
import { SearchQuery } from '../schemas/queries/search.query';
|
|
3
3
|
import { SearchMutation } from '../schemas/mutations/search.mutation';
|
|
4
4
|
import { BaseProvider } from './base.provider';
|
|
5
|
+
import { CacheEvaluation } from '../cache/cache-evaluation.interface';
|
|
6
|
+
import { Session } from '../schemas/session.schema';
|
|
5
7
|
export declare abstract class SearchProvider<T extends SearchResult = SearchResult, Q extends SearchQuery = SearchQuery, M extends SearchMutation = SearchMutation> extends BaseProvider<T, Q, M> {
|
|
8
|
+
protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
|
|
6
9
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { BaseQuery } from "../schemas/queries/base.query";
|
|
2
|
-
import { Session } from "../schemas/session.schema";
|
|
3
|
-
export interface CachingStrategyEvaluation {
|
|
4
|
-
key: string;
|
|
5
|
-
cacheDurationInSeconds: number;
|
|
6
|
-
canCache: boolean;
|
|
7
|
-
}
|
|
8
|
-
export interface CachingStrategy {
|
|
9
|
-
get(query: BaseQuery, session: Session): CachingStrategyEvaluation;
|
|
10
|
-
}
|
|
11
|
-
export declare class BaseCachingStrategy implements CachingStrategy {
|
|
12
|
-
get(query: BaseQuery, session: Session): CachingStrategyEvaluation;
|
|
13
|
-
}
|