UNPKG

@bolttech/templating-sdk

Version:

JavaScript SDK for Bolttech Templating Service - Create, manage and render templates with ease

680 lines (659 loc) 22.5 kB
import axios from 'axios'; import Handlebars from 'handlebars'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __classPrivateFieldGet(receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); } function __classPrivateFieldSet(receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var _Template_sdkInstance; /** * Represents a template with rendering capabilities */ class Template { constructor(template, engine, sdkInstance, isModified = false, originalTemplate) { _Template_sdkInstance.set(this, void 0); // Private field - completely hidden from public API this._template = Object.freeze({ ...template }); this._engine = engine; this._isModified = isModified; this._originalTemplate = originalTemplate || { ...template }; __classPrivateFieldSet(this, _Template_sdkInstance, sdkInstance, "f"); } /** * Get template ID */ get id() { return this._template._id; } /** * Get template slug */ get slug() { return this._template.slug; } /** * Get template title */ get title() { return this._template.title; } /** * Get template format */ get format() { return this._template.format; } /** * Get template content */ get content() { return this._template.content; } /** * Get template tenant */ get tenant() { return this._template.tenant; } /** * Get creation date */ get createdAt() { return new Date(this._template.createdAt); } /** * Get last update date */ get updatedAt() { return new Date(this._template.updatedAt); } /** * Check if template has been modified locally but not yet saved to the API */ get isModified() { return this._isModified; } /** * Render the template with given placeholders using the template engine */ render(placeholders = {}, options = {}) { return this._engine.render(this._template.content, placeholders, options); } /** * Create a new Template instance with updated properties * This method is immutable - returns a new instance with changes */ update(updates) { const newTemplateData = { ...this._template, ...updates, }; if (!this._hasChangesFromOriginal(newTemplateData)) { // No actual changes from original, return template with isModified = false return new Template(newTemplateData, this._engine, __classPrivateFieldGet(this, _Template_sdkInstance, "f"), false, this._originalTemplate); } // Has changes, update timestamp and mark as modified const finalTemplateData = { ...newTemplateData, updatedAt: new Date().toISOString(), }; // Retorna uma NOVA instância return new Template(finalTemplateData, this._engine, __classPrivateFieldGet(this, _Template_sdkInstance, "f"), true, this._originalTemplate); } /** * Save the current template to the service * Requires SDK instance to be set */ async save() { if (!__classPrivateFieldGet(this, _Template_sdkInstance, "f")) { throw new Error('Cannot save template: SDK instance not available. Template must be created through TemplatingSdk.'); } if (!this.isModified) { // No changes to save return this; } this._validate(); try { // Use the SDK to update the template const updatedTemplate = await __classPrivateFieldGet(this, _Template_sdkInstance, "f").update(this.id, { title: this.title, format: this.format, content: this.content, slug: this.slug, }); // The saved template should not be marked as modified and becomes the new original Object.assign(this, new Template(updatedTemplate._template, updatedTemplate._engine, __classPrivateFieldGet(this, _Template_sdkInstance, "f"), false, updatedTemplate._template)); return this; } catch (error) { throw new Error(`Failed to save template: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Validate update data */ _validate() { if (this.title !== undefined && (!this.title || this.title.trim().length === 0)) { throw new Error('Template title cannot be empty'); } if (this.format !== undefined && (!this.format || this.format.trim().length === 0)) { throw new Error('Template format cannot be empty'); } if (this.slug !== undefined && this.slug !== null) { const slugRegex = /^[a-z0-9-]+$/; if (!slugRegex.test(this.slug)) { throw new Error('Template slug must contain only lowercase letters, numbers, and hyphens'); } } } /** * Check if the new template data has changes from the original template */ _hasChangesFromOriginal(newTemplateData) { // Compare only the fields that can be updated const fieldsToCompare = ['title', 'content', 'format', 'slug']; return fieldsToCompare.some((field) => { // @ts-ignore: acessar dynamicamente os templates return this._originalTemplate[field] !== newTemplateData[field]; }); } } _Template_sdkInstance = new WeakMap(); /** * Abstract base class for template engines */ class TemplateEngine { } /** * Base error class for all templating SDK errors */ class BaseError extends Error { constructor(message, statusCode, metadata) { super(message); this.statusCode = statusCode; this.metadata = metadata; this.name = this.constructor.name; Error.captureStackTrace?.(this, this.constructor); } toJSON() { return { name: this.name, message: this.message, code: this.code, statusCode: this.statusCode, metadata: this.metadata, stack: this.stack, }; } } /** * Base error for template engine operations */ class TemplateEngineError extends BaseError { constructor(message, format, templateContent, placeholders) { super(message, undefined, { format, templateContent, placeholders }); this.format = format; this.templateContent = templateContent; this.placeholders = placeholders; this.code = 'TEMPLATE_ENGINE_ERROR'; } } /** * Template render error */ class TemplateRenderError extends BaseError { constructor(format, originalError, templateContent, placeholders) { super(`Template render failed: ${originalError.message}`, undefined, { format, templateContent, placeholders, originalError: originalError.message, }); this.code = 'TEMPLATE_RENDER_ERROR'; } } /** * Template compilation error */ class TemplateCompilationError extends BaseError { constructor(format, originalError, templateContent) { super(`Template compilation failed: ${originalError.message}`, undefined, { format, templateContent, originalError: originalError.message, }); this.code = 'TEMPLATE_COMPILATION_ERROR'; } } /** * Unsupported template format error */ class UnsupportedFormatError extends BaseError { constructor(format, supportedFormats) { super(`Unsupported template format: ${format}. Supported formats: ${supportedFormats.join(', ')}`, undefined, { format, supportedFormats }); this.code = 'UNSUPPORTED_FORMAT_ERROR'; } } /** * General templating service error */ class TemplatingError extends BaseError { constructor(message, statusCode, response) { super(message, statusCode, { response }); this.response = response; this.code = 'TEMPLATING_ERROR'; } } /** * Template not found error */ class TemplateNotFoundError extends BaseError { constructor(identifier) { super(`Template not found: ${identifier}`, 404, { identifier }); this.code = 'TEMPLATE_NOT_FOUND'; } } /** * Template validation error */ class TemplateValidationError extends BaseError { constructor(message, validationErrors) { super(message, 400, { validationErrors }); this.code = 'TEMPLATE_VALIDATION_ERROR'; } } /** * Authentication error */ class AuthenticationError extends BaseError { constructor(message = 'Authentication failed') { super(message, 401); this.code = 'AUTHENTICATION_ERROR'; } } /** * Authorization error */ class AuthorizationError extends BaseError { constructor(message = 'Access denied') { super(message, 403); this.code = 'AUTHORIZATION_ERROR'; } } /** * Network/connection error */ class NetworkError extends BaseError { constructor(message, originalError) { super(message, undefined, { originalError: originalError?.message }); this.code = 'NETWORK_ERROR'; } } /** * Request timeout error */ class TimeoutError extends BaseError { constructor(timeout) { super(`Request timeout after ${timeout}ms`); this.code = 'TIMEOUT_ERROR'; } } /** * Error handler utility for HTTP responses and axios errors */ class ErrorHandler { /** * Handle axios errors and convert to appropriate error types */ static handleAxiosError(error) { if (error.code === 'ECONNABORTED') { throw new TimeoutError(parseInt(error.config?.timeout?.toString() || '0')); } if (!error.response) { throw new NetworkError(error.message, error); } const { status, data } = error.response; const message = data?.metadata?.message || error.message; switch (status) { case 400: if (data?.metadata?.error) { throw new TemplateValidationError(message, data.metadata.error); } throw new TemplatingError(message, status, data); case 401: throw new AuthenticationError(message); case 403: throw new AuthorizationError(message); case 404: throw new TemplateNotFoundError(extractIdentifierFromError(error, message)); case 422: throw new TemplateValidationError(message, data?.metadata?.error); case 500: case 502: case 503: case 504: throw new NetworkError(message, error); default: throw new TemplatingError(message, status, data); } } /** * Check if error is a known templating error */ static isTemplatingError(error) { return error instanceof TemplatingError; } /** * Check if error is retryable (network/timeout errors) */ static isRetryableError(error) { return error instanceof NetworkError || error instanceof TimeoutError; } /** * Get error details for logging */ static getErrorDetails(error) { if (error instanceof TemplatingError) { return { name: error.name, code: error.code, message: error.message, statusCode: error.statusCode, metadata: error.metadata, }; } if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack, }; } return { error: String(error), }; } } /** * Extract identifier from error context (URL, message, etc.) */ function extractIdentifierFromError(error, message) { // Try to extract from URL path const urlMatch = error.config?.url?.match(/\/templates\/([^\/]+)/); if (urlMatch) { return urlMatch[1]; } // Try to extract from message const messageMatch = message.match(/Template not found: (.+)/); if (messageMatch) { return messageMatch[1]; } return 'unknown'; } // Import for type guards // Type guards for error checking function isTemplatingError(error) { return error instanceof TemplatingError; } function isTemplateNotFoundError(error) { return error instanceof TemplateNotFoundError; } function isTemplateValidationError(error) { return error instanceof TemplateValidationError; } function isNetworkError(error) { return error instanceof NetworkError; } function isTimeoutError(error) { return error instanceof TimeoutError; } function isTemplateEngineError(error) { return error instanceof TemplateEngineError; } function isTemplateRenderError(error) { return error instanceof TemplateRenderError; } class HandlebarsEngine extends TemplateEngine { constructor() { super(); // Create an isolated Handlebars instance this.instance = Handlebars.create(); this.setupDefaultHelpers(); } /** * Renders a template with placeholders */ render(template, placeholders = {}, options = {}) { try { // Compile and render const compiled = this.instance.compile(template); return compiled(placeholders); } catch (error) { throw new TemplateRenderError('handlebars', error, template, placeholders); } } /** * Configura helpers padrão úteis */ setupDefaultHelpers() { // Helper for date formatting this.instance.registerHelper('formatDate', function (date, format) { const d = new Date(date); if (isNaN(d.getTime())) return ''; switch (format) { case 'short': return d.toLocaleDateString(); case 'long': return d.toLocaleDateString('pt-BR', { year: 'numeric', month: 'long', day: 'numeric', }); case 'time': return d.toLocaleTimeString(); case 'iso': return d.toISOString(); default: return d.toLocaleDateString(); } }); // Helper for currency formatting this.instance.registerHelper('currency', function (value, currency = 'BRL') { if (typeof value !== 'number') return ''; return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: currency, }).format(value); }); // Helper for text capitalization this.instance.registerHelper('capitalize', function (text) { if (typeof text !== 'string') return ''; return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); }); // Helper for equality conditional this.instance.registerHelper('eq', function (a, b) { return a === b; }); // Helper for inequality conditional this.instance.registerHelper('ne', function (a, b) { return a !== b; }); // Helper para condicional maior que this.instance.registerHelper('gt', function (a, b) { return a > b; }); // Helper para condicional menor que this.instance.registerHelper('lt', function (a, b) { return a < b; }); // Helper para fallback de valores this.instance.registerHelper('default', function (value, defaultValue) { return value || defaultValue; }); } } class RawEngine extends TemplateEngine { /** * For raw templates, simply returns the original content */ render(template, placeholders = {}, options = {}) { return template; } } class TemplateEngineFactory { /** * Obtém o engine apropriado para um template */ static getEngine(template) { const engineKey = this.formatToEngineMap[template.format.toLowerCase()] || 'raw'; const engine = this.engines.get(engineKey); if (!engine) { throw new UnsupportedFormatError(template.format, Object.keys(this.formatToEngineMap)); } return engine; } /** * Lists available engines */ static getAvailableEngines() { return Array.from(this.engines.keys()); } } TemplateEngineFactory.engines = new Map([ ['handlebars', new HandlebarsEngine()], ['raw', new RawEngine()], ]); TemplateEngineFactory.formatToEngineMap = { html: 'handlebars', text: 'raw', }; class TemplateFactory { /** * Creates a Template with the appropriate engine */ static create(template, sdkInstance) { const engine = TemplateEngineFactory.getEngine(template); return new Template(template, engine, sdkInstance); } } /** * Main SDK class for interacting with the Templating Service */ class TemplatingSdk { constructor(baseUrl, tenantId, options = {}) { this.config = { baseUrl: baseUrl.replace(/\/$/, ''), // Remove trailing slash tenantId, timeout: options.timeout || 30000, headers: options.headers || {}, }; this.client = axios.create({ baseURL: this.config.baseUrl, timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', 'x-tenant': this.config.tenantId, ...this.config.headers, }, }); // Add response interceptor for error handling this.client.interceptors.response.use((response) => response, (error) => { ErrorHandler.handleAxiosError(error); }); } /** * Get a template by ID or slug */ async get(identifier) { const response = await this.client.get(`/v1/templates/${identifier}`); return TemplateFactory.create(response.data.payload, this); } /** * Get all templates */ async getAll() { const response = await this.client.get('/v1/templates'); return response.data.payload.map((template) => TemplateFactory.create(template, this)); } /** * Create a new template */ async create(templateData) { const response = await this.client.post('/v1/templates', templateData); return TemplateFactory.create(response.data.payload, this); } /** * Update an existing template */ async update(id, templateData) { const response = await this.client.put(`/v1/templates/${id}`, templateData); return TemplateFactory.create(response.data.payload, this); } /** * Delete a template */ async delete(identifier) { await this.client.delete(`/v1/templates/${identifier}`); return true; } /** * Check service health */ async healthCheck() { try { const response = await this.client.get('/healthcheck'); return response.status === 200; } catch { return false; } } /** * Update client configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; // Update axios instance this.client.defaults.baseURL = this.config.baseUrl; this.client.defaults.timeout = this.config.timeout; // Update headers one by one to avoid type issues this.client.defaults.headers['x-tenant'] = this.config.tenantId; if (this.config.headers) { Object.assign(this.client.defaults.headers, this.config.headers); } } /** * Get current configuration */ getConfig() { return { ...this.config }; } } export { AuthenticationError, AuthorizationError, BaseError, ErrorHandler, NetworkError, Template, TemplateCompilationError, TemplateEngineError, TemplateNotFoundError, TemplateRenderError, TemplateValidationError, TemplatingError, TemplatingSdk, TimeoutError, UnsupportedFormatError, isNetworkError, isTemplateEngineError, isTemplateNotFoundError, isTemplateRenderError, isTemplateValidationError, isTemplatingError, isTimeoutError }; //# sourceMappingURL=index.esm.js.map