mcard-js
Version:
MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers
187 lines • 10.1 kB
JavaScript
/**
* VCard Application Vocabulary - Data-driven patterns for side effects.
*
* Resource types are defined as DATA in vcard_ext/, making the system
* fully extensible without code changes.
*/
import { HashValidator } from './hash/HashValidator';
import { RESOURCE_REGISTRY, ResourceCategory } from './vcard_ext/index.js';
export { ResourceCategory, RESOURCE_REGISTRY };
// =============================================================================
// Core Resource Factory
// =============================================================================
export class Resource {
static async create(resourceType, ...argsAndOptions) {
if (!(resourceType in RESOURCE_REGISTRY)) {
throw new Error(`Unknown resource type: ${resourceType}. Available: ${Object.keys(RESOURCE_REGISTRY).join(', ')}`);
}
const rtype = RESOURCE_REGISTRY[resourceType];
// Separate positional args from options
const positionalArgs = [];
let options = {};
for (const arg of argsAndOptions) {
if (typeof arg === 'object' && !Array.isArray(arg) && arg !== null) {
options = { ...options, ...arg };
}
else {
positionalArgs.push(arg);
}
}
options = { ...rtype.defaultOptions, ...options };
// Map positional args using type's argNames
const templateVars = { ...options };
for (let i = 0; i < positionalArgs.length && i < rtype.argNames.length; i++) {
templateVars[rtype.argNames[i]] = positionalArgs[i];
}
const uri = Resource._buildUri(rtype.uriTemplate, templateVars);
const hashContent = Resource._buildHashContent(rtype.hashTemplate, templateVars);
const contentHash = await HashValidator.computeHash(new TextEncoder().encode(hashContent), 'sha256');
const status = Resource._determineStatus(resourceType, templateVars, options);
const qosMetrics = { category: rtype.category };
for (const [k, v] of Object.entries(templateVars)) {
if (v !== undefined && v !== null && !k.startsWith('_')) {
qosMetrics[k] = v;
}
}
return { uri, contentHash, status, qosMetrics };
}
static _buildUri(template, vars) {
const preserveUri = vars._preserveUri || false;
let uri = template;
for (const [key, value] of Object.entries(vars)) {
if (value !== undefined && value !== null && !key.startsWith('_')) {
let strVal = String(value);
if (!preserveUri && (key === 'endpoint' || key === 'collectorUrl')) {
strVal = strVal.replace('http://', '').replace('https://', '');
}
uri = uri.replace(`{${key}}`, strVal);
}
}
return uri;
}
static _buildHashContent(template, vars) {
let content = template;
for (const [key, value] of Object.entries(vars)) {
content = content.replace(`{${key}}`, value !== undefined ? String(value) : 'undefined');
}
return content;
}
static _determineStatus(resourceType, vars, options) {
const required = options.required !== false;
if (resourceType === 'env') {
const name = vars.name || '';
const actualValue = typeof process !== 'undefined' ? (process.env[name] ?? options.default) : undefined;
if (actualValue !== undefined)
return 'verified';
return required ? 'invalid' : 'pending';
}
return 'pending';
}
static types() { return Object.keys(RESOURCE_REGISTRY); }
static register(type) { RESOURCE_REGISTRY[type.name] = type; }
}
// =============================================================================
// Helper Functions
// =============================================================================
export async function createLgtmStack(baseUrl, serviceName, options = {}) {
const { environment = 'production', ports = {} } = options;
const p = { grafana: 3000, prometheus: 9090, loki: 3100, tempo: 4317, ...ports };
return Promise.all([
Resource.create('grafana', `${baseUrl}:${p.grafana}`, { description: `Grafana for ${serviceName}` }),
Resource.create('prometheus', `${baseUrl}:${p.prometheus}`, { jobName: serviceName }),
Resource.create('loki', `${baseUrl}:${p.loki}`, { labels: { service: serviceName, environment } }),
Resource.create('tempo', `${baseUrl}:${p.tempo}`, { serviceName })
]);
}
// =============================================================================
// Convenience Wrappers
// =============================================================================
export var AccessMode;
(function (AccessMode) {
AccessMode["READ"] = "read";
AccessMode["WRITE"] = "write";
AccessMode["READ_WRITE"] = "rw";
AccessMode["EXECUTE"] = "execute";
})(AccessMode || (AccessMode = {}));
export class EnvResource {
static async create(name, options = {}) {
return Resource.create('env', name, options);
}
static resolve(ref) {
if (!ref.uri.startsWith('env://'))
throw new Error(`Not an env:// URI: ${ref.uri}`);
return typeof process !== 'undefined' ? process.env[ref.uri.replace('env://', '')] : undefined;
}
}
export class FileResource {
static async create(path, options = {}) {
return Resource.create('file', path, { ...options, mode: options.mode || AccessMode.READ });
}
static async createDirectory(path, options = {}) {
return Resource.create('directory', path, options);
}
}
export class StorageResource {
static async createSqlite(path, options = {}) { return Resource.create('sqlite', path, options); }
static async createPostgres(conn, options = {}) { return Resource.create('postgres', conn, options); }
static async createS3(bucket, key, options = {}) { return Resource.create('s3', bucket, key, options); }
static async createLitefs(path, options = {}) { return Resource.create('litefs', path, options); }
static async createTurso(db, options = {}) { return Resource.create('turso', db, options); }
}
export class NetworkResource {
static async createApi(endpoint, options = {}) { return Resource.create('api', endpoint, options); }
static async createWebhook(endpoint, options = {}) { return Resource.create('webhook', endpoint, options); }
}
export class ObservabilityResource {
static async createGrafana(endpoint, options = {}) { return Resource.create('grafana', endpoint, { ...options, endpoint }); }
static async createPrometheus(endpoint, options = {}) { return Resource.create('prometheus', endpoint, { ...options, endpoint }); }
static async createLoki(endpoint, options = {}) { return Resource.create('loki', endpoint, { ...options, endpoint }); }
static async createTempo(endpoint, options = {}) { return Resource.create('tempo', endpoint, { ...options, endpoint }); }
static async createFaro(collectorUrl, appName, options = {}) { return Resource.create('faro', collectorUrl, appName, { ...options, collectorUrl }); }
static async createOtlp(endpoint, options = {}) { return Resource.create('otlp', endpoint, { ...options, endpoint }); }
static async createLgtmStack(baseUrl, serviceName, options) { return createLgtmStack(baseUrl, serviceName, options); }
}
// =============================================================================
// Application Resources Manager
// =============================================================================
export class ApplicationResources {
resources = [];
async add(resourceType, ...argsAndOptions) {
this.resources.push(await Resource.create(resourceType, ...argsAndOptions));
return this;
}
async addEnv(name, options) { return this.add('env', name, options || {}); }
async addFile(path, options) { return this.add('file', path, options || {}); }
async addDirectory(path, options) { return this.add('directory', path, options || {}); }
async addSqlite(path, options) { return this.add('sqlite', path, options || {}); }
async addLitefs(path, options) { return this.add('litefs', path, options || {}); }
async addTurso(db, options) { return this.add('turso', db, options || {}); }
async addApi(endpoint, options) { return this.add('api', endpoint, options || {}); }
async addGrafana(endpoint, options) { return this.add('grafana', endpoint, options || {}); }
async addPrometheus(endpoint, options) { return this.add('prometheus', endpoint, options || {}); }
async addLoki(endpoint, options) { return this.add('loki', endpoint, options || {}); }
async addTempo(endpoint, options) { return this.add('tempo', endpoint, options || {}); }
async addFaro(url, app, options) { this.resources.push(await ObservabilityResource.createFaro(url, app, options)); return this; }
async addOtlp(endpoint, options) { return this.add('otlp', endpoint, options || {}); }
async addLgtmStack(baseUrl, serviceName, options) {
this.resources.push(...await createLgtmStack(baseUrl, serviceName, options));
return this;
}
getAll() { return this.resources; }
getByCategory(category) {
const cat = typeof category === 'string' ? category : category;
return this.resources.filter(r => r.qosMetrics?.category === cat);
}
getObservabilityResources() { return this.resources.filter(r => r.qosMetrics?.category === 'observability'); }
getInvalid() { return this.resources.filter(r => r.status === 'invalid'); }
validate() {
const invalid = this.getInvalid();
return {
valid: invalid.length === 0, total: this.resources.length,
verified: this.resources.filter(r => r.status === 'verified').length,
pending: this.resources.filter(r => r.status === 'pending').length,
invalid: invalid.length, invalidResources: invalid.map(r => r.uri)
};
}
}
//# sourceMappingURL=vcard_vocabulary.js.map