@mixpeek/contentful 1.0.0
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/api/mixpeekClient.js +111 -0
- package/dist/cache/cacheManager.js +90 -0
- package/dist/config/constants.js +48 -0
- package/dist/index.cjs +8 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +46 -0
- package/dist/modules/contentEnricher.js +92 -0
- package/dist/modules/contentfulClient.js +92 -0
- package/dist/modules/webhookHandler.js +92 -0
- package/dist/utils/helpers.js +49 -0
- package/dist/utils/logger.js +63 -0
- package/package.json +67 -0
- package/src/api/mixpeekClient.js +111 -0
- package/src/cache/cacheManager.js +90 -0
- package/src/config/constants.js +48 -0
- package/src/index.js +46 -0
- package/src/modules/contentEnricher.js +92 -0
- package/src/modules/contentfulClient.js +92 -0
- package/src/modules/webhookHandler.js +92 -0
- package/src/utils/helpers.js +49 -0
- package/src/utils/logger.js +63 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — WebhookHandler
|
|
3
|
+
*
|
|
4
|
+
* Handles Contentful webhooks (entry publish/unpublish/archive) and triggers enrichment
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createClient } from '../api/mixpeekClient.js';
|
|
8
|
+
import { createCacheManager } from '../cache/cacheManager.js';
|
|
9
|
+
import { getLogger } from '../utils/logger.js';
|
|
10
|
+
import { DEFAULT_CONFIG } from '../config/constants.js';
|
|
11
|
+
|
|
12
|
+
class WebhookHandler {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} config
|
|
15
|
+
* @param {string} config.apiKey - Mixpeek API key
|
|
16
|
+
* @param {string} [config.endpoint] - API endpoint
|
|
17
|
+
* @param {number} [config.timeout] - Request timeout in ms
|
|
18
|
+
* @param {number} [config.cacheTTL] - Cache TTL in seconds
|
|
19
|
+
* @param {boolean} [config.enableCache] - Enable caching
|
|
20
|
+
* @param {boolean} [config.debug] - Enable debug logging
|
|
21
|
+
*/
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
if (!config.apiKey) throw new Error('apiKey is required for WebhookHandler');
|
|
24
|
+
|
|
25
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
26
|
+
this.client = createClient({
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
endpoint: this.config.endpoint,
|
|
29
|
+
timeout: this.config.timeout,
|
|
30
|
+
debug: this.config.debug
|
|
31
|
+
});
|
|
32
|
+
this.cache = this.config.enableCache ? createCacheManager({ ttl: this.config.cacheTTL, debug: this.config.debug }) : null;
|
|
33
|
+
this.logger = getLogger({ debug: this.config.debug });
|
|
34
|
+
this.metrics = { requests: 0, errors: 0, totalLatencyMs: 0 };
|
|
35
|
+
this.logger.info('WebhookHandler initialized');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async handleWebhook(...args) {
|
|
39
|
+
this.logger.debug('WebhookHandler.handleWebhook called');
|
|
40
|
+
// TODO: Implement handleWebhook
|
|
41
|
+
throw new Error('WebhookHandler.handleWebhook not yet implemented');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
verifySignature(...args) {
|
|
45
|
+
this.logger.debug('WebhookHandler.verifySignature called');
|
|
46
|
+
// TODO: Implement verifySignature
|
|
47
|
+
throw new Error('WebhookHandler.verifySignature not yet implemented');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async registerWebhook(...args) {
|
|
51
|
+
this.logger.debug('WebhookHandler.registerWebhook called');
|
|
52
|
+
// TODO: Implement registerWebhook
|
|
53
|
+
throw new Error('WebhookHandler.registerWebhook not yet implemented');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async listWebhooks(...args) {
|
|
57
|
+
this.logger.debug('WebhookHandler.listWebhooks called');
|
|
58
|
+
// TODO: Implement listWebhooks
|
|
59
|
+
throw new Error('WebhookHandler.listWebhooks not yet implemented');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async deleteWebhook(...args) {
|
|
63
|
+
this.logger.debug('WebhookHandler.deleteWebhook called');
|
|
64
|
+
// TODO: Implement deleteWebhook
|
|
65
|
+
throw new Error('WebhookHandler.deleteWebhook not yet implemented');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getMetrics() {
|
|
69
|
+
return {
|
|
70
|
+
...this.metrics,
|
|
71
|
+
avgLatencyMs: this.metrics.requests > 0 ? this.metrics.totalLatencyMs / this.metrics.requests : 0,
|
|
72
|
+
cache: this.cache ? this.cache.getStats() : null
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resetMetrics() {
|
|
77
|
+
this.metrics = { requests: 0, errors: 0, totalLatencyMs: 0 };
|
|
78
|
+
if (this.cache) this.cache.resetStats();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
destroy() {
|
|
82
|
+
if (this.cache) this.cache.destroy();
|
|
83
|
+
this.logger.info('WebhookHandler destroyed');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createWebhookHandler(config) {
|
|
88
|
+
return new WebhookHandler(config);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { WebhookHandler };
|
|
92
|
+
export default { createWebhookHandler, WebhookHandler };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — Helper Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function generateId() {
|
|
6
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createCacheKey(content) {
|
|
10
|
+
const str = JSON.stringify(content);
|
|
11
|
+
let hash = 0;
|
|
12
|
+
for (let i = 0; i < str.length; i++) {
|
|
13
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
14
|
+
hash = hash & hash;
|
|
15
|
+
}
|
|
16
|
+
return `mixpeek_${Math.abs(hash).toString(36)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sanitizeText(text, maxLength = 50000) {
|
|
20
|
+
if (!text || typeof text !== 'string') return '';
|
|
21
|
+
return text.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().substring(0, maxLength);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deepMerge(target, source) {
|
|
25
|
+
const result = { ...target };
|
|
26
|
+
for (const key of Object.keys(source)) {
|
|
27
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
28
|
+
result[key] = target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
|
|
29
|
+
? deepMerge(target[key], source[key]) : { ...source[key] };
|
|
30
|
+
} else if (Array.isArray(source[key])) {
|
|
31
|
+
result[key] = [...source[key]];
|
|
32
|
+
} else {
|
|
33
|
+
result[key] = source[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isValidUrl(url) {
|
|
40
|
+
if (!url || typeof url !== 'string') return false;
|
|
41
|
+
try { new URL(url); return true; } catch { return false; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function extractDomain(url) {
|
|
45
|
+
if (!isValidUrl(url)) return null;
|
|
46
|
+
try { return new URL(url).hostname; } catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default { generateId, createCacheKey, sanitizeText, deepMerge, isValidUrl, extractDomain };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — Logger Utility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4 };
|
|
6
|
+
|
|
7
|
+
class Logger {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.prefix = options.prefix || '[Mixpeek-Contentful]';
|
|
10
|
+
this.level = options.debug ? LOG_LEVELS.DEBUG : LOG_LEVELS.INFO;
|
|
11
|
+
this.enabled = options.enabled !== false;
|
|
12
|
+
this.timers = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setLevel(level) {
|
|
16
|
+
this.level = typeof level === 'string' ? (LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.INFO) : level;
|
|
17
|
+
}
|
|
18
|
+
setEnabled(enabled) { this.enabled = enabled; }
|
|
19
|
+
|
|
20
|
+
_log(level, levelName, ...args) {
|
|
21
|
+
if (!this.enabled || level < this.level) return;
|
|
22
|
+
const ts = new Date().toISOString();
|
|
23
|
+
const formatted = args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : a);
|
|
24
|
+
const msg = `${ts} ${this.prefix} [${levelName}]`;
|
|
25
|
+
if (level === LOG_LEVELS.ERROR) console.error(msg, ...formatted);
|
|
26
|
+
else if (level === LOG_LEVELS.WARN) console.warn(msg, ...formatted);
|
|
27
|
+
else if (level === LOG_LEVELS.DEBUG) console.debug(msg, ...formatted);
|
|
28
|
+
else console.log(msg, ...formatted);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
debug(...args) { this._log(LOG_LEVELS.DEBUG, 'DEBUG', ...args); }
|
|
32
|
+
info(...args) { this._log(LOG_LEVELS.INFO, 'INFO', ...args); }
|
|
33
|
+
warn(...args) { this._log(LOG_LEVELS.WARN, 'WARN', ...args); }
|
|
34
|
+
error(...args) { this._log(LOG_LEVELS.ERROR, 'ERROR', ...args); }
|
|
35
|
+
|
|
36
|
+
time(label) { this.timers.set(label, performance.now ? performance.now() : Date.now()); }
|
|
37
|
+
timeEnd(label) {
|
|
38
|
+
const start = this.timers.get(label);
|
|
39
|
+
if (!start) return 0;
|
|
40
|
+
const elapsed = (performance.now ? performance.now() : Date.now()) - start;
|
|
41
|
+
this.timers.delete(label);
|
|
42
|
+
this.debug(`${label}: ${elapsed.toFixed(2)}ms`);
|
|
43
|
+
return elapsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
child(subPrefix) {
|
|
47
|
+
return new Logger({ prefix: `${this.prefix}[${subPrefix}]`, debug: this.level === LOG_LEVELS.DEBUG, enabled: this.enabled });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let defaultLogger = null;
|
|
52
|
+
export function getLogger(options = {}) {
|
|
53
|
+
if (!defaultLogger) { defaultLogger = new Logger(options); }
|
|
54
|
+
else if (Object.keys(options).length > 0) {
|
|
55
|
+
if (options.debug !== undefined) defaultLogger.setLevel(options.debug ? 'DEBUG' : 'INFO');
|
|
56
|
+
if (options.enabled !== undefined) defaultLogger.setEnabled(options.enabled);
|
|
57
|
+
}
|
|
58
|
+
return defaultLogger;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createLogger(options = {}) { return new Logger(options); }
|
|
62
|
+
export { Logger, LOG_LEVELS };
|
|
63
|
+
export default { getLogger, createLogger, Logger, LOG_LEVELS };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mixpeek/contentful",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Contentful integration for Mixpeek — webhook handling, content enrichment, and management API integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "src/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node scripts/build.js",
|
|
23
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js",
|
|
24
|
+
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js --testPathPattern=tests/unit",
|
|
25
|
+
"test:e2e": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.e2e.js --testPathPattern=tests/e2e",
|
|
26
|
+
"test:live": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.live.js --testPathPattern=tests/live-api",
|
|
27
|
+
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js --watch",
|
|
28
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js --coverage",
|
|
29
|
+
"lint": "eslint src tests",
|
|
30
|
+
"lint:fix": "eslint src tests --fix",
|
|
31
|
+
"prepublishOnly": "npm run build && npm test"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"contentful",
|
|
35
|
+
"cms",
|
|
36
|
+
"headless-cms",
|
|
37
|
+
"content-enrichment",
|
|
38
|
+
"webhook",
|
|
39
|
+
"content-management",
|
|
40
|
+
"mixpeek",
|
|
41
|
+
"multimodal",
|
|
42
|
+
"enrichment",
|
|
43
|
+
"connector"
|
|
44
|
+
],
|
|
45
|
+
"author": "Mixpeek <info@mixpeek.com>",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/mixpeek/connectors.git",
|
|
50
|
+
"directory": "contentful"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/mixpeek/connectors/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/mixpeek/connectors/tree/main/contentful#readme",
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=14.0.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"jest": "^29.7.0",
|
|
61
|
+
"eslint": "^8.57.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"contentful-management": ">=10.0.0"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {}
|
|
67
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — Mixpeek API Client
|
|
3
|
+
*
|
|
4
|
+
* HTTP client for Mixpeek API integration with retry logic and timeout handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
API_ENDPOINT, API_VERSION, DEFAULT_TIMEOUT,
|
|
9
|
+
RETRY_ATTEMPTS, RETRY_DELAY, ERROR_CODES, HEADERS
|
|
10
|
+
} from '../config/constants.js';
|
|
11
|
+
import { getLogger } from '../utils/logger.js';
|
|
12
|
+
|
|
13
|
+
class MixpeekClient {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} config
|
|
16
|
+
* @param {string} config.apiKey - Mixpeek API key
|
|
17
|
+
* @param {string} [config.endpoint] - API endpoint
|
|
18
|
+
* @param {number} [config.timeout] - Request timeout in ms
|
|
19
|
+
* @param {boolean} [config.debug] - Enable debug logging
|
|
20
|
+
*/
|
|
21
|
+
constructor(config) {
|
|
22
|
+
if (!config.apiKey) throw new Error('API key is required');
|
|
23
|
+
|
|
24
|
+
this.apiKey = config.apiKey;
|
|
25
|
+
this.endpoint = (config.endpoint || API_ENDPOINT).replace(/\/$/, '');
|
|
26
|
+
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
27
|
+
this.logger = getLogger({ debug: config.debug });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async request(method, path, body = null) {
|
|
31
|
+
const url = `${this.endpoint}/${API_VERSION}${path}`;
|
|
32
|
+
const headers = {
|
|
33
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
34
|
+
'Content-Type': HEADERS.CONTENT_TYPE,
|
|
35
|
+
'Accept': HEADERS.ACCEPT,
|
|
36
|
+
'User-Agent': HEADERS.USER_AGENT
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let lastError;
|
|
40
|
+
for (let attempt = 0; attempt <= RETRY_ATTEMPTS; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
44
|
+
|
|
45
|
+
this.logger.debug(`API ${method} ${path} (attempt ${attempt + 1})`);
|
|
46
|
+
|
|
47
|
+
const response = await fetch(url, {
|
|
48
|
+
method,
|
|
49
|
+
headers,
|
|
50
|
+
body: body ? JSON.stringify(body) : null,
|
|
51
|
+
signal: controller.signal
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const errorBody = await response.text();
|
|
58
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return await response.json();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
lastError = error;
|
|
64
|
+
if (error.name === 'AbortError') {
|
|
65
|
+
lastError = new Error(`Request timeout after ${this.timeout}ms`);
|
|
66
|
+
lastError.code = ERROR_CODES.TIMEOUT;
|
|
67
|
+
}
|
|
68
|
+
if (attempt < RETRY_ATTEMPTS) {
|
|
69
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.logger.error('API request failed after retries:', lastError.message);
|
|
75
|
+
throw lastError;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async healthCheck() {
|
|
79
|
+
try {
|
|
80
|
+
const start = Date.now();
|
|
81
|
+
const response = await this.request('GET', '/health');
|
|
82
|
+
return { status: 'healthy', latency: Date.now() - start, timestamp: new Date().toISOString(), response };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return { status: 'unhealthy', error: error.message, timestamp: new Date().toISOString() };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async search(query, options = {}) {
|
|
89
|
+
return this.request('POST', '/features/search', { query, ...options });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getDocument(collectionId, documentId) {
|
|
93
|
+
return this.request('GET', `/collections/${collectionId}/documents/${documentId}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async createDocument(collectionId, payload) {
|
|
97
|
+
return this.request('POST', `/collections/${collectionId}/documents`, payload);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async listDocuments(collectionId, options = {}) {
|
|
101
|
+
const params = new URLSearchParams(options).toString();
|
|
102
|
+
return this.request('GET', `/collections/${collectionId}/documents${params ? '?' + params : ''}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createClient(config) {
|
|
107
|
+
return new MixpeekClient(config);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { MixpeekClient };
|
|
111
|
+
export default { createClient, MixpeekClient };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — Cache Manager
|
|
3
|
+
*
|
|
4
|
+
* In-memory LRU caching with TTL support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_CACHE_TTL, MAX_CACHE_ITEMS, CACHE_KEY_PREFIX } from '../config/constants.js';
|
|
8
|
+
import { getLogger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
class CacheManager {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.ttl = (options.ttl || DEFAULT_CACHE_TTL) * 1000;
|
|
13
|
+
this.maxItems = options.maxItems || MAX_CACHE_ITEMS;
|
|
14
|
+
this.cache = new Map();
|
|
15
|
+
this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 };
|
|
16
|
+
this.logger = getLogger({ debug: options.debug });
|
|
17
|
+
this.cleanupInterval = setInterval(() => this.prune(), 60000);
|
|
18
|
+
if (this.cleanupInterval.unref) this.cleanupInterval.unref();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_prefixKey(key) { return `${CACHE_KEY_PREFIX}${key}`; }
|
|
22
|
+
|
|
23
|
+
get(key) {
|
|
24
|
+
const item = this.cache.get(this._prefixKey(key));
|
|
25
|
+
if (!item) { this.stats.misses++; return null; }
|
|
26
|
+
if (Date.now() > item.expiry) { this.cache.delete(this._prefixKey(key)); this.stats.misses++; return null; }
|
|
27
|
+
this.stats.hits++;
|
|
28
|
+
item.lastAccess = Date.now();
|
|
29
|
+
return item.data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set(key, data, ttl = null) {
|
|
33
|
+
if (this.cache.size >= this.maxItems) this._evictLRU();
|
|
34
|
+
this.cache.set(this._prefixKey(key), {
|
|
35
|
+
data, expiry: Date.now() + (ttl ? ttl * 1000 : this.ttl),
|
|
36
|
+
createdAt: Date.now(), lastAccess: Date.now()
|
|
37
|
+
});
|
|
38
|
+
this.stats.sets++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
has(key) {
|
|
42
|
+
const item = this.cache.get(this._prefixKey(key));
|
|
43
|
+
if (!item) return false;
|
|
44
|
+
if (Date.now() > item.expiry) { this.cache.delete(this._prefixKey(key)); return false; }
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
delete(key) { return this.cache.delete(this._prefixKey(key)); }
|
|
49
|
+
clear() { this.cache.clear(); }
|
|
50
|
+
|
|
51
|
+
prune() {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
let pruned = 0;
|
|
54
|
+
for (const [key, item] of this.cache.entries()) {
|
|
55
|
+
if (now > item.expiry) { this.cache.delete(key); pruned++; }
|
|
56
|
+
}
|
|
57
|
+
return pruned;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_evictLRU() {
|
|
61
|
+
let oldestKey = null, oldestAccess = Infinity;
|
|
62
|
+
for (const [key, item] of this.cache.entries()) {
|
|
63
|
+
if (item.lastAccess < oldestAccess) { oldestAccess = item.lastAccess; oldestKey = key; }
|
|
64
|
+
}
|
|
65
|
+
if (oldestKey) { this.cache.delete(oldestKey); this.stats.evictions++; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getStats() {
|
|
69
|
+
const total = this.stats.hits + this.stats.misses;
|
|
70
|
+
return {
|
|
71
|
+
size: this.cache.size, maxSize: this.maxItems,
|
|
72
|
+
hits: this.stats.hits, misses: this.stats.misses,
|
|
73
|
+
hitRate: total > 0 ? Math.round((this.stats.hits / total) * 10000) / 100 : 0,
|
|
74
|
+
sets: this.stats.sets, evictions: this.stats.evictions, ttlSeconds: this.ttl / 1000
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
resetStats() { this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 }; }
|
|
79
|
+
|
|
80
|
+
destroy() {
|
|
81
|
+
if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; }
|
|
82
|
+
this.cache.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get size() { return this.cache.size; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createCacheManager(options = {}) { return new CacheManager(options); }
|
|
89
|
+
export { CacheManager };
|
|
90
|
+
export default { createCacheManager, CacheManager };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — Configuration Constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// API Configuration
|
|
6
|
+
export const API_ENDPOINT = 'https://api.mixpeek.com';
|
|
7
|
+
export const API_VERSION = 'v1';
|
|
8
|
+
export const DEFAULT_TIMEOUT = 30000; // milliseconds
|
|
9
|
+
export const MAX_TIMEOUT = 60000;
|
|
10
|
+
export const RETRY_ATTEMPTS = 1;
|
|
11
|
+
export const RETRY_DELAY = 100; // milliseconds
|
|
12
|
+
|
|
13
|
+
// Cache Configuration
|
|
14
|
+
export const DEFAULT_CACHE_TTL = 300; // seconds (5 minutes)
|
|
15
|
+
export const MAX_CACHE_ITEMS = 1000;
|
|
16
|
+
export const CACHE_KEY_PREFIX = 'mixpeek_ctfl_';
|
|
17
|
+
|
|
18
|
+
// Error Codes
|
|
19
|
+
export const ERROR_CODES = {
|
|
20
|
+
API_ERROR: 'MIXPEEK_API_ERROR',
|
|
21
|
+
TIMEOUT: 'MIXPEEK_TIMEOUT',
|
|
22
|
+
INVALID_CONFIG: 'MIXPEEK_INVALID_CONFIG',
|
|
23
|
+
INVALID_REQUEST: 'MIXPEEK_INVALID_REQUEST',
|
|
24
|
+
RATE_LIMITED: 'MIXPEEK_RATE_LIMITED',
|
|
25
|
+
CACHE_ERROR: 'MIXPEEK_CACHE_ERROR'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// HTTP Headers
|
|
29
|
+
export const HEADERS = {
|
|
30
|
+
CONTENT_TYPE: 'application/json',
|
|
31
|
+
ACCEPT: 'application/json',
|
|
32
|
+
USER_AGENT: 'Mixpeek-Contentful-Connector/1.0.0'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Default Configuration
|
|
36
|
+
export const DEFAULT_CONFIG = {
|
|
37
|
+
endpoint: API_ENDPOINT,
|
|
38
|
+
timeout: DEFAULT_TIMEOUT,
|
|
39
|
+
cacheTTL: DEFAULT_CACHE_TTL,
|
|
40
|
+
enableCache: true,
|
|
41
|
+
debug: false
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
API_ENDPOINT, API_VERSION, DEFAULT_TIMEOUT, MAX_TIMEOUT,
|
|
46
|
+
RETRY_ATTEMPTS, RETRY_DELAY, DEFAULT_CACHE_TTL, MAX_CACHE_ITEMS,
|
|
47
|
+
CACHE_KEY_PREFIX, ERROR_CODES, HEADERS, DEFAULT_CONFIG
|
|
48
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — Mixpeek Contentful Connector
|
|
3
|
+
*
|
|
4
|
+
* Contentful integration for Mixpeek — webhook handling, content enrichment, and management API integration
|
|
5
|
+
*
|
|
6
|
+
* @module @mixpeek/contentful
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Modules
|
|
11
|
+
export { createWebhookHandler, WebhookHandler } from './modules/webhookHandler.js';
|
|
12
|
+
export { createContentEnricher, ContentEnricher } from './modules/contentEnricher.js';
|
|
13
|
+
export { createContentfulClient, ContentfulClient } from './modules/contentfulClient.js';
|
|
14
|
+
|
|
15
|
+
// API client
|
|
16
|
+
export { createClient, MixpeekClient } from './api/mixpeekClient.js';
|
|
17
|
+
|
|
18
|
+
// Cache manager
|
|
19
|
+
export { createCacheManager, CacheManager } from './cache/cacheManager.js';
|
|
20
|
+
|
|
21
|
+
// Utilities
|
|
22
|
+
export {
|
|
23
|
+
generateId,
|
|
24
|
+
createCacheKey,
|
|
25
|
+
sanitizeText,
|
|
26
|
+
deepMerge,
|
|
27
|
+
isValidUrl,
|
|
28
|
+
extractDomain
|
|
29
|
+
} from './utils/helpers.js';
|
|
30
|
+
|
|
31
|
+
// Logger
|
|
32
|
+
export { getLogger, createLogger, Logger, LOG_LEVELS } from './utils/logger.js';
|
|
33
|
+
|
|
34
|
+
// Constants
|
|
35
|
+
export {
|
|
36
|
+
API_ENDPOINT,
|
|
37
|
+
API_VERSION,
|
|
38
|
+
DEFAULT_TIMEOUT,
|
|
39
|
+
DEFAULT_CACHE_TTL,
|
|
40
|
+
ERROR_CODES,
|
|
41
|
+
DEFAULT_CONFIG
|
|
42
|
+
} from './config/constants.js';
|
|
43
|
+
|
|
44
|
+
// Default export
|
|
45
|
+
import { createWebhookHandler } from './modules/webhookHandler.js';
|
|
46
|
+
export default createWebhookHandler;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mixpeek/contentful — ContentEnricher
|
|
3
|
+
*
|
|
4
|
+
* Enriches Contentful entries with Mixpeek multimodal analysis stored in custom fields
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createClient } from '../api/mixpeekClient.js';
|
|
8
|
+
import { createCacheManager } from '../cache/cacheManager.js';
|
|
9
|
+
import { getLogger } from '../utils/logger.js';
|
|
10
|
+
import { DEFAULT_CONFIG } from '../config/constants.js';
|
|
11
|
+
|
|
12
|
+
class ContentEnricher {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} config
|
|
15
|
+
* @param {string} config.apiKey - Mixpeek API key
|
|
16
|
+
* @param {string} [config.endpoint] - API endpoint
|
|
17
|
+
* @param {number} [config.timeout] - Request timeout in ms
|
|
18
|
+
* @param {number} [config.cacheTTL] - Cache TTL in seconds
|
|
19
|
+
* @param {boolean} [config.enableCache] - Enable caching
|
|
20
|
+
* @param {boolean} [config.debug] - Enable debug logging
|
|
21
|
+
*/
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
if (!config.apiKey) throw new Error('apiKey is required for ContentEnricher');
|
|
24
|
+
|
|
25
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
26
|
+
this.client = createClient({
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
endpoint: this.config.endpoint,
|
|
29
|
+
timeout: this.config.timeout,
|
|
30
|
+
debug: this.config.debug
|
|
31
|
+
});
|
|
32
|
+
this.cache = this.config.enableCache ? createCacheManager({ ttl: this.config.cacheTTL, debug: this.config.debug }) : null;
|
|
33
|
+
this.logger = getLogger({ debug: this.config.debug });
|
|
34
|
+
this.metrics = { requests: 0, errors: 0, totalLatencyMs: 0 };
|
|
35
|
+
this.logger.info('ContentEnricher initialized');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async enrichEntry(...args) {
|
|
39
|
+
this.logger.debug('ContentEnricher.enrichEntry called');
|
|
40
|
+
// TODO: Implement enrichEntry
|
|
41
|
+
throw new Error('ContentEnricher.enrichEntry not yet implemented');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async enrichAsset(...args) {
|
|
45
|
+
this.logger.debug('ContentEnricher.enrichAsset called');
|
|
46
|
+
// TODO: Implement enrichAsset
|
|
47
|
+
throw new Error('ContentEnricher.enrichAsset not yet implemented');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async enrichBatch(...args) {
|
|
51
|
+
this.logger.debug('ContentEnricher.enrichBatch called');
|
|
52
|
+
// TODO: Implement enrichBatch
|
|
53
|
+
throw new Error('ContentEnricher.enrichBatch not yet implemented');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getEnrichment(...args) {
|
|
57
|
+
this.logger.debug('ContentEnricher.getEnrichment called');
|
|
58
|
+
// TODO: Implement getEnrichment
|
|
59
|
+
throw new Error('ContentEnricher.getEnrichment not yet implemented');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async writeField(...args) {
|
|
63
|
+
this.logger.debug('ContentEnricher.writeField called');
|
|
64
|
+
// TODO: Implement writeField
|
|
65
|
+
throw new Error('ContentEnricher.writeField not yet implemented');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getMetrics() {
|
|
69
|
+
return {
|
|
70
|
+
...this.metrics,
|
|
71
|
+
avgLatencyMs: this.metrics.requests > 0 ? this.metrics.totalLatencyMs / this.metrics.requests : 0,
|
|
72
|
+
cache: this.cache ? this.cache.getStats() : null
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resetMetrics() {
|
|
77
|
+
this.metrics = { requests: 0, errors: 0, totalLatencyMs: 0 };
|
|
78
|
+
if (this.cache) this.cache.resetStats();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
destroy() {
|
|
82
|
+
if (this.cache) this.cache.destroy();
|
|
83
|
+
this.logger.info('ContentEnricher destroyed');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createContentEnricher(config) {
|
|
88
|
+
return new ContentEnricher(config);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { ContentEnricher };
|
|
92
|
+
export default { createContentEnricher, ContentEnricher };
|