UNPKG

@mercnet/core

Version:

Core utilities and types for MercNet ecosystem

729 lines (724 loc) 21.6 kB
import { GraphQLClient } from 'graphql-request'; import gql from 'graphql-tag'; import { z } from 'zod'; export { z } from 'zod'; // @mercnet/core - Built with tsup // src/utils/index.ts var capitalize = (str) => { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); }; var camelCase = (str) => { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => { return index === 0 ? word.toLowerCase() : word.toUpperCase(); }).replace(/\s+/g, ""); }; var pick = (obj, keys) => { const result = {}; keys.forEach((key) => { if (key in obj) { result[key] = obj[key]; } }); return result; }; var isObject = (item) => { return item !== null && typeof item === "object" && !Array.isArray(item); }; var isString = (value) => { return typeof value === "string"; }; var isNumber = (value) => { return typeof value === "number" && !isNaN(value); }; var isBoolean = (value) => { return typeof value === "boolean"; }; var isFunction = (value) => { return typeof value === "function"; }; var isArray = (value) => { return Array.isArray(value); }; var unique = (array) => { return Array.from(new Set(array)); }; var sleep = (ms) => { return new Promise((resolve) => globalThis.setTimeout(resolve, ms)); }; var retry = async (fn, options) => { let lastError; for (let i = 0; i <= options.retries; i++) { try { return await fn(); } catch (error) { lastError = error; if (i < options.retries) { const delay = options.delay || 1e3; const backoff = options.backoff || 1; await sleep(delay * Math.pow(backoff, i)); } } } throw lastError; }; var formatDate = (date, format = "YYYY-MM-DD") => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return format.replace("YYYY", String(year)).replace("MM", month).replace("DD", day).replace("HH", hours).replace("mm", minutes).replace("ss", seconds); }; var isValidDate = (date) => { return date instanceof Date && !isNaN(date.getTime()); }; var getEnv = (key, defaultValue) => { if (typeof process !== "undefined" && process.env) { return process.env[key] || defaultValue || ""; } return defaultValue || ""; }; var isDevelopment = () => { return getEnv("NODE_ENV") === "development"; }; var isProduction = () => { return getEnv("NODE_ENV") === "production"; }; var MercNetError = class extends Error { constructor(message, code, details) { super(message); this.code = code; this.details = details; this.name = "MercNetError"; } }; var createError = (message, code, details) => { return new MercNetError(message, code, details); }; var GetAllMarketsDocument = gql` query GetAllMarkets { marketCollection { edges { node { id name short_name ticker price type created_at last_update_at trend_status sentiment sentiment_update_at market_activity_score_1h market_activity_score_1d data status } } } } `; var GetMarketByIdDocument = gql` query GetMarketById($id: Int!) { marketCollection(filter: { id: { eq: $id } }) { edges { node { id name short_name ticker price type created_at last_update_at trend_status sentiment sentiment_update_at market_activity_score_1h market_activity_score_1d data status } } } } `; var GetMarketByTickerDocument = gql` query GetMarketByTicker($ticker: String!) { marketCollection(filter: { ticker: { eq: $ticker } }) { edges { node { id name short_name ticker price type created_at last_update_at trend_status sentiment sentiment_update_at market_activity_score_1h market_activity_score_1d data status } } } } `; var GetMarketsByStatusDocument = gql` query GetMarketsByStatus($status: String!) { marketCollection(filter: { status: { eq: $status } }) { edges { node { id name short_name ticker price type created_at last_update_at trend_status sentiment sentiment_update_at market_activity_score_1h market_activity_score_1d data status } } } } `; var defaultWrapper = (action, _operationName, _operationType, _variables) => action(); function getSdk(client, withWrapper = defaultWrapper) { return { GetAllMarkets(variables, requestHeaders, signal) { return withWrapper( (wrappedRequestHeaders) => client.request({ document: GetAllMarketsDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "GetAllMarkets", "query", variables ); }, GetMarketById(variables, requestHeaders, signal) { return withWrapper( (wrappedRequestHeaders) => client.request({ document: GetMarketByIdDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "GetMarketById", "query", variables ); }, GetMarketByTicker(variables, requestHeaders, signal) { return withWrapper( (wrappedRequestHeaders) => client.request({ document: GetMarketByTickerDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "GetMarketByTicker", "query", variables ); }, GetMarketsByStatus(variables, requestHeaders, signal) { return withWrapper( (wrappedRequestHeaders) => client.request({ document: GetMarketsByStatusDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "GetMarketsByStatus", "query", variables ); } }; } // src/graphql/index.ts var DEFAULT_CONFIG = { retries: 3, debug: false }; var MercNetGraphQLClient = class { client; config; sdk; constructor(config) { this.config = { ...DEFAULT_CONFIG, ...config }; this.client = new GraphQLClient(this.config.endpoint, { headers: this.config.headers }); this.sdk = getSdk(this.client); } // Update headers (useful for authentication) setHeaders(headers) { this.client.setHeaders(headers); } // Add a single header setHeader(key, value) { this.client.setHeader(key, value); } // Execute a GraphQL query async query(query, variables, requestHeaders) { try { if (this.config.debug) { console.log("[GraphQL Query]", { query, variables }); } const response = await this.client.request(query, variables, requestHeaders); if (this.config.debug) { console.log("[GraphQL Response]", response); } return response; } catch (error) { if (this.config.debug) { console.error("[GraphQL Error]", error); } throw this.handleGraphQLError(error); } } // Execute a GraphQL mutation async mutate(mutation, variables, requestHeaders) { return this.query(mutation, variables, requestHeaders); } // Execute raw request with full response async rawRequest(query, variables, requestHeaders) { try { const response = await this.client.rawRequest(query, variables, requestHeaders); return { data: response.data, errors: response.errors }; } catch (error) { throw this.handleGraphQLError(error); } } // Typed methods for market operations using generated SDK async getAllMarkets() { try { if (this.config.debug) { console.log("[GraphQL] Fetching all markets"); } const response = await this.sdk.GetAllMarkets(); const markets = this.transformMarketsResponse(response); if (this.config.debug) { console.log("[GraphQL] Retrieved markets:", markets.length); } return markets; } catch (error) { throw this.handleGraphQLError(error); } } async getMarketById(id) { try { if (this.config.debug) { console.log("[GraphQL] Fetching market by ID:", id); } const response = await this.sdk.GetMarketById({ id }); const markets = this.transformMarketsResponse(response); const market = markets.length > 0 ? markets[0] : null; if (this.config.debug) { console.log("[GraphQL] Retrieved market:", market ? market.id : "not found"); } return market; } catch (error) { throw this.handleGraphQLError(error); } } async getMarketByTicker(ticker) { try { if (this.config.debug) { console.log("[GraphQL] Fetching market by ticker:", ticker); } const response = await this.sdk.GetMarketByTicker({ ticker }); const markets = this.transformMarketsResponse(response); const market = markets.length > 0 ? markets[0] : null; if (this.config.debug) { console.log("[GraphQL] Retrieved market by ticker:", market ? market.ticker : "not found"); } return market; } catch (error) { throw this.handleGraphQLError(error); } } async getMarketsByStatus(status) { try { if (this.config.debug) { console.log("[GraphQL] Fetching markets by status:", status); } const response = await this.sdk.GetMarketsByStatus({ status }); const markets = this.transformMarketsResponse(response); if (this.config.debug) { console.log("[GraphQL] Retrieved markets by status:", markets.length); } return markets; } catch (error) { throw this.handleGraphQLError(error); } } // Transform GraphQL response to Market array transformMarketsResponse(response) { if (!response.marketCollection?.edges) { return []; } return response.marketCollection.edges.filter((edge) => edge?.node).map((edge) => { const node = edge.node; return { id: node.id, name: node.name, short_name: node.short_name, ticker: node.ticker, price: node.price, type: node.type, created_at: node.created_at, last_update_at: node.last_update_at, trend_status: node.trend_status, sentiment: node.sentiment, sentiment_update_at: node.sentiment_update_at, market_activity_score_1h: node.market_activity_score_1h, market_activity_score_1d: node.market_activity_score_1d, data: node.data, status: node.status }; }); } // Handle GraphQL errors handleGraphQLError(error) { if (error.response?.errors) { const graphqlErrors = error.response.errors; const message = graphqlErrors.map((e) => e.message).join(", "); return new MercNetError(message, "GRAPHQL_ERROR", { errors: graphqlErrors, query: error.request?.query, variables: error.request?.variables }); } if (error.message) { return new MercNetError(error.message, "GRAPHQL_NETWORK_ERROR", { originalError: error }); } return new MercNetError("Unknown GraphQL error", "GRAPHQL_UNKNOWN_ERROR", { originalError: error }); } }; var createGraphQLClient = (config) => { return new MercNetGraphQLClient(config); }; var COMMON_FRAGMENTS = { // Market fragment MARKET_FRAGMENT: ` fragment MarketFragment on market { id name short_name ticker price type created_at last_update_at trend_status sentiment sentiment_update_at market_activity_score_1h market_activity_score_1d data status } `, // Pagination info fragment PAGINATION_INFO: ` fragment PaginationInfo on PageInfo { hasNextPage hasPreviousPage startCursor endCursor } `, // Error fragment ERROR_INFO: ` fragment ErrorInfo on Error { message code details } ` }; var QueryBuilder = class { fields = []; fragments = []; variables = []; // Add a field to the query field(name, subFields) { if (subFields) { this.fields.push(`${name} { ${subFields.join(" ")} }`); } else { this.fields.push(name); } return this; } // Add a fragment to the query fragment(name, fragment) { this.fragments.push(fragment); this.fields.push(`...${name}`); return this; } // Add a variable to the query variable(name, type) { this.variables.push(`$${name}: ${type}`); return this; } // Build the final query build(operationType = "query", operationName) { const variablesPart = this.variables.length > 0 ? `(${this.variables.join(", ")})` : ""; const operationPart = operationName ? `${operationType} ${operationName}${variablesPart}` : `${operationType}${variablesPart}`; const query = ` ${operationPart} { ${this.fields.join("\n")} } ${this.fragments.join("\n")} `; return query.trim(); } }; var createQueryBuilder = () => { return new QueryBuilder(); }; var gql2 = (strings, ...values) => { return strings.reduce((result, string, index) => { return result + string + (values[index] || ""); }, ""); }; var getSchemaInfo = async (client) => { const introspectionQuery = gql2` query IntrospectionQuery { __schema { types { name kind } queryType { fields { name } } mutationType { fields { name } } subscriptionType { fields { name } } } } `; const response = await client.query(introspectionQuery); const schema = response.__schema; return { types: schema.types.map((type) => type.name), queries: schema.queryType?.fields?.map((field) => field.name) || [], mutations: schema.mutationType?.fields?.map((field) => field.name) || [], subscriptions: schema.subscriptionType?.fields?.map((field) => field.name) || [] }; }; var baseEntitySchema = z.object({ id: z.string().min(1, "ID is required"), createdAt: z.date(), updatedAt: z.date() }); var paginationParamsSchema = z.object({ page: z.number().int().positive().optional().default(1), limit: z.number().int().positive().max(100).optional().default(20), offset: z.number().int().nonnegative().optional() }); var apiResponseSchema = (dataSchema) => z.object({ data: dataSchema.optional(), error: z.object({ code: z.string(), message: z.string(), details: z.record(z.unknown()).optional() }).optional(), success: z.boolean(), message: z.string().optional() }); var validators = { email: z.string().email("Invalid email format"), password: z.string().min(8, "Password must be at least 8 characters"), url: z.string().url("Invalid URL format"), phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format"), uuid: z.string().uuid("Invalid UUID format"), slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Invalid slug format"), hex: z.string().regex(/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, "Invalid hex color"), // Date validators dateString: z.string().datetime("Invalid date format"), dateRange: z.object({ start: z.date(), end: z.date() }).refine((data) => data.end > data.start, { message: "End date must be after start date", path: ["end"] }), // Number validators positiveNumber: z.number().positive("Must be a positive number"), nonNegativeNumber: z.number().nonnegative("Must be a non-negative number"), percentage: z.number().min(0).max(100, "Must be between 0 and 100"), currency: z.number().multipleOf(0.01, "Invalid currency format"), // String validators nonEmptyString: z.string().min(1, "Cannot be empty"), alphanumeric: z.string().regex(/^[a-zA-Z0-9]+$/, "Must contain only letters and numbers"), noWhitespace: z.string().regex(/^\S+$/, "Cannot contain whitespace") }; var environmentSchema = z.enum(["development", "production", "test", "staging"]); var baseConfigSchema = z.object({ environment: environmentSchema, apiUrl: validators.url, graphqlUrl: validators.url.optional(), debug: z.boolean().optional().default(false) }); var ValidationError = class extends MercNetError { constructor(message, issues) { super(message, "VALIDATION_ERROR", { issues }); this.issues = issues; this.name = "ValidationError"; } }; var validate = (schema, data, options) => { const result = schema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } const error = new ValidationError( options?.errorMessage || "Validation failed", result.error.issues ); if (options?.throwOnError) { throw error; } return { success: false, error }; }; var validateAndThrow = (schema, data, errorMessage) => { const result = validate(schema, data, { throwOnError: true, errorMessage }); return result.data; }; var createArraySchema = (itemSchema, options) => { let schema = z.array(itemSchema); if (options?.minLength !== void 0) { schema = schema.min(options.minLength, `Must have at least ${options.minLength} items`); } if (options?.maxLength !== void 0) { schema = schema.max(options.maxLength, `Must have at most ${options.maxLength} items`); } if (options?.unique) { schema = schema.refine( (items) => { const set = new Set(items.map((item) => JSON.stringify(item))); return set.size === items.length; }, { message: "Items must be unique" } ); } return schema; }; var createOptionalWithDefault = (schema, defaultValue) => { return schema.optional().default(defaultValue); }; var transforms = { stringToNumber: z.string().transform((val, ctx) => { const parsed = parseFloat(val); if (isNaN(parsed)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Not a number" }); return z.NEVER; } return parsed; }), stringToBoolean: z.string().transform((val) => { return val.toLowerCase() === "true" || val === "1"; }), stringToDate: z.string().transform((val, ctx) => { const date = new Date(val); if (isNaN(date.getTime())) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid date" }); return z.NEVER; } return date; }), trimString: z.string().transform((val) => val.trim()), toLowerCase: z.string().transform((val) => val.toLowerCase()), toUpperCase: z.string().transform((val) => val.toUpperCase()) }; var createConditionalSchema = (baseSchema, conditions) => { return baseSchema.superRefine((data, ctx) => { for (const condition of conditions) { if (condition.when(data)) { const result = condition.then.safeParse(data); if (!result.success) { result.error.issues.forEach((issue) => { ctx.addIssue({ ...issue, message: condition.message || issue.message }); }); } } } }); }; var customValidators = { strongPassword: z.string().refine( (password) => { const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); const hasNumbers = /\d/.test(password); const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); return hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChar; }, { message: "Password must contain uppercase, lowercase, number and special character" } ), uniqueArray: (itemSchema, keyExtractor) => z.array(itemSchema).refine( (items) => { if (!keyExtractor) { return new Set(items).size === items.length; } const keys = items.map(keyExtractor); return new Set(keys).size === keys.length; }, { message: "Array items must be unique" } ), futureDate: z.date().refine((date) => date > /* @__PURE__ */ new Date(), { message: "Date must be in the future" }), pastDate: z.date().refine((date) => date < /* @__PURE__ */ new Date(), { message: "Date must be in the past" }) }; // src/index.ts var VERSION = "0.1.0"; export { apiResponseSchema as ApiResponseSchema, baseConfigSchema as BaseConfigSchema, baseEntitySchema as BaseEntitySchema, COMMON_FRAGMENTS, environmentSchema as EnvironmentSchema, MercNetError, MercNetGraphQLClient, paginationParamsSchema as PaginationParamsSchema, QueryBuilder, VERSION, ValidationError, camelCase, capitalize, createArraySchema, createConditionalSchema, createError, createGraphQLClient, createOptionalWithDefault, createQueryBuilder, customValidators, formatDate, getEnv, getSchemaInfo, gql2 as gql, isArray, isBoolean, isDevelopment, isFunction, isNumber, isObject, isProduction, isString, isValidDate, pick, retry, sleep, transforms, unique, validate, validateAndThrow, validators }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map