import { UrlLoader } from '@graphql-tools/url-loader'; import { invert, cloneDeep, isEmpty, isArray, map, isObject, isDate, isFunction, extend, mapValues, set } from 'lodash'; import { URL as URL$1 } from 'url'; import 'isomorphic-fetch'; import { sign, decode } from 'jsonwebtoken'; import { GraphQLClient } from 'graphql-request'; import chalk from 'chalk'; import HttpsProxyAgent from 'https-proxy-agent'; import HttpProxyAgent from 'http-proxy-agent'; import Ajv from 'ajv'; import { safeLoad, safeDump } from 'js-yaml'; import { accessSync, readFileSync, writeFileSync, mkdirSync, promises } from 'fs'; import replaceall from 'replaceall'; import { resolve, all, reduce } from 'bluebird'; import stringify from 'json-stable-stringify'; import { config } from 'dotenv'; import { dirname, resolve as resolve$1, join } from 'path'; import { safeLoad as safeLoad$1 } from 'yaml-ast-parser'; import { homedir } from 'os'; import { cwd } from 'process'; const cloudApiEndpoint = process.env.CLOUD_API_ENDPOINT || 'https://api.cloud.prisma.sh'; const clusterEndpointMap = { 'prisma-eu1': 'https://eu1.prisma.sh', 'prisma-us1': 'https://us1.prisma.sh', }; const clusterEndpointMapReverse = invert(clusterEndpointMap); function getClusterName(origin) { if (clusterEndpointMapReverse[origin]) { return clusterEndpointMapReverse[origin]; } if (origin.endsWith('prisma.sh')) { return origin.split('_')[0].replace(/https?:\/\//, ''); } if (isLocal(origin)) { return 'local'; } return 'default'; } const getWorkspaceFromPrivateOrigin = (origin) => { const split = origin.split('_'); if (split.length > 1) { return split[1].split('.')[0]; } return null; }; const isLocal = (origin) => origin.includes('localhost') || origin.includes('127.0.0.1'); function parseEndpoint(endpoint) { /* Terminology: local - hosted locally using docker and accessed using localhost or prisma or local web proxy like domain.dev shared - demo server isPrivate - private hosted by Prisma or private and self-hosted, important that in our terminology a local server is not private */ const url = new URL$1(endpoint); const splittedPath = url.pathname.split('/'); // assuming, that the pathname always starts with a leading /, we always can ignore the first element of the split array const service = splittedPath.length > 3 ? splittedPath[2] : splittedPath[1] || 'default'; const stage = splittedPath.length > 3 ? splittedPath[3] : splittedPath[2] || 'default'; // This logic might break for self-hosted servers incorrectly yielding a "workspace" simply if the UX has // enough "/"es like if https://custom.dev/not-a-workspace/ is the base Prisma URL then for default/default service/stage // pair. This function would incorrectly return not-a-workspace as a workspace. let workspaceSlug = splittedPath.length > 3 ? splittedPath[1] : null; const shared = ['eu1.prisma.sh', 'us1.prisma.sh'].includes(url.host); // When using localAliases, do an exact match because of 'prisma' option which is added for local docker networking access const localAliases = ['localhost', '127.0.0.1', 'prisma']; const isPrivate = !shared && !localAliases.includes(url.hostname); const local = !shared && !isPrivate && !workspaceSlug; if (isPrivate && !workspaceSlug) { workspaceSlug = getWorkspaceFromPrivateOrigin(url.origin); } return { clusterBaseUrl: url.origin, service, stage, local, isPrivate, shared, workspaceSlug, clusterName: getClusterName(url.origin), }; } // code from https://raw.githubusercontent.com/request/request/5ba8eb44da7cd639ca21070ea9be20d611b85f66/lib/getProxyFromURI.js function formatHostname(hostname) { // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' return hostname.replace(/^\.*/, '.').toLowerCase(); } function parseNoProxyZone(zone) { zone = zone.trim().toLowerCase(); const zoneParts = zone.split(':', 2); const zoneHost = formatHostname(zoneParts[0]); const zonePort = zoneParts[1]; const hasPort = zone.indexOf(':') > -1; return { hostname: zoneHost, port: zonePort, hasPort: hasPort }; } function uriInNoProxy(uri, noProxy) { const port = uri.port || (uri.protocol === 'https:' ? '443' : '80'); const hostname = formatHostname(uri.hostname); const noProxyList = noProxy.split(','); // iterate through the noProxyList until it finds a match. return noProxyList.map(parseNoProxyZone).some(function (noProxyZone) { const isMatchedAt = hostname.indexOf(noProxyZone.hostname); const hostnameMatched = isMatchedAt > -1 && isMatchedAt === hostname.length - noProxyZone.hostname.length; if (noProxyZone.hasPort) { return port === noProxyZone.port && hostnameMatched; } return hostnameMatched; }); } function getProxyFromURI(uri) { // Decide the proper request proxy to use based on the request URI object and the // environmental variables (NO_PROXY, HTTP_PROXY, etc.) // respect NO_PROXY environment variables (see: http://lynx.isc.org/current/breakout/lynx_help/keystrokes/environments.html) const noProxy = process.env.NO_PROXY || process.env.no_proxy || ''; // if the noProxy is a wildcard then return null if (noProxy === '*') { return null; } // if the noProxy is not empty and the uri is found return null if (noProxy !== '' && uriInNoProxy(uri, noProxy)) { return null; } // Check for HTTP or HTTPS Proxy in environment Else default to null if (uri.protocol === 'http:') { return process.env.HTTP_PROXY || process.env.http_proxy || null; } if (uri.protocol === 'https:') { return (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null); } // if none of that works, return null // (What uri protocol are you using then?) return null; } function getProxyAgent(url) { const uri = new URL(url); const proxy = getProxyFromURI(uri); if (!proxy) { return undefined; } const proxyUri = new URL(proxy); if (proxyUri.protocol === 'http:') { // eslint-disable-next-line // @ts-ignore return new HttpProxyAgent(proxy); } if (proxyUri.protocol === 'https:') { // eslint-disable-next-line // @ts-ignore return new HttpsProxyAgent(proxy); } return undefined; } const debug = require('debug')('environment'); class Cluster { constructor(out, name, baseUrl, clusterSecret, local = true, shared = false, isPrivate = false, workspaceSlug) { this.out = out; this.name = name; // All `baseUrl` extension points in this class // adds a trailing slash. Here we remove it from // the passed `baseUrl` in order to avoid double // slashes. this.baseUrl = baseUrl.replace(/\/$/, ''); this.clusterSecret = clusterSecret; this.local = local; this.shared = shared; this.isPrivate = isPrivate; this.workspaceSlug = workspaceSlug; this.hasOldDeployEndpoint = false; } async getToken(serviceName, workspaceSlug, stageName) { // public clusters just take the token const needsAuth = await this.needsAuth(); debug({ needsAuth }); if (!needsAuth) { return null; } if (this.name === 'shared-public-demo') { return ''; } if (this.isPrivate && process.env.PRISMA_MANAGEMENT_API_SECRET) { return this.getLocalToken(); } if (this.shared || (this.isPrivate && !process.env.PRISMA_MANAGEMENT_API_SECRET)) { return this.generateClusterToken(serviceName, workspaceSlug, stageName); } else { return this.getLocalToken(); } } getLocalToken() { if (!this.clusterSecret && !process.env.PRISMA_MANAGEMENT_API_SECRET) { return null; } if (!this.cachedToken) { const grants = [{ target: `*/*`, action: '*' }]; const secret = process.env.PRISMA_MANAGEMENT_API_SECRET || this.clusterSecret; try { const algorithm = process.env.PRISMA_MANAGEMENT_API_SECRET ? 'HS256' : 'RS256'; this.cachedToken = sign({ grants }, secret, { expiresIn: '5y', algorithm, }); } catch (e) { throw new Error(`Could not generate token for cluster ${chalk.bold(this.getDeployEndpoint())}. Did you provide the env var PRISMA_MANAGEMENT_API_SECRET? Original error: ${e.message}`); } } return this.cachedToken; } get cloudClient() { return new GraphQLClient(cloudApiEndpoint, { headers: { Authorization: `Bearer ${this.clusterSecret}`, }, agent: getProxyAgent(cloudApiEndpoint), }); } async generateClusterToken(serviceName, workspaceSlug = this.workspaceSlug || '*', stageName) { const query = ` mutation ($input: GenerateClusterTokenRequest!) { generateClusterToken(input: $input) { clusterToken } } `; const { generateClusterToken: { clusterToken }, } = await this.cloudClient.request(query, { input: { workspaceSlug, clusterName: this.name, serviceName, stageName, }, }); return clusterToken; } async addServiceToCloudDBIfMissing(serviceName, workspaceSlug = this.workspaceSlug, stageName) { const query = ` mutation ($input: GenerateClusterTokenRequest!) { addServiceToCloudDBIfMissing(input: $input) } `; const serviceCreated = await this.cloudClient.request(query, { input: { workspaceSlug, clusterName: this.name, serviceName, stageName, }, }); return serviceCreated.addServiceToCloudDBIfMissing; } getApiEndpoint(service, stage, workspaceSlug) { if (!this.shared && service === 'default' && stage === 'default') { return this.baseUrl; } if (!this.shared && stage === 'default') { return `${this.baseUrl}/${service}`; } if (this.isPrivate || this.local) { return `${this.baseUrl}/${service}/${stage}`; } const workspaceString = workspaceSlug ? `${workspaceSlug}/` : ''; return `${this.baseUrl}/${workspaceString}${service}/${stage}`; } getWSEndpoint(service, stage, workspaceSlug) { return this.getApiEndpoint(service, stage, workspaceSlug).replace(/^http/, 'ws'); } getImportEndpoint(service, stage, workspaceSlug) { return this.getApiEndpoint(service, stage, workspaceSlug) + `/import`; } getExportEndpoint(service, stage, workspaceSlug) { return this.getApiEndpoint(service, stage, workspaceSlug) + `/export`; } getDeployEndpoint() { return `${this.baseUrl}/${this.hasOldDeployEndpoint ? 'cluster' : 'management'}`; } async isOnline() { const version = await this.getVersion(); return typeof version === 'string'; } async getVersion() { // first try new api try { const result = await this.request(`{ serverInfo { version } }`); const res = await result.json(); const { data, errors } = res; if (errors && errors[0].code === 3016 && errors[0].message.includes('management@default')) { this.hasOldDeployEndpoint = true; return await this.getVersion(); } if (data && data.serverInfo) { return data.serverInfo.version; } } catch (e) { debug(e); } // if that doesn't work, try the old one try { const result = await this.request(`{ serverInfo { version } }`); const res = await result.json(); const { data } = res; return data.serverInfo.version; } catch (e) { debug(e); } return null; } request(query, variables) { return fetch(this.getDeployEndpoint(), { method: 'post', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables, }), agent: getProxyAgent(this.getDeployEndpoint()), }); } async needsAuth() { try { const result = await this.request(`{ listProjects { name } }`); const data = await result.json(); if (data.errors && data.errors.length > 0) { return true; } return false; } catch (e) { debug('Assuming that the server needs authentication'); debug(e.toString()); return true; } } toJSON() { return { name: this.name, baseUrl: this.baseUrl, local: this.local, clusterSecret: this.clusterSecret, shared: this.shared, isPrivate: this.isPrivate, workspaceSlug: this.workspaceSlug, }; } } const schema = JSON.parse(`{ "$schema": "http://json-schema.org/draft-07/schema#", "title": "JSON schema for Prisma prisma.yml files", "definitions": { "subscription": { "description": "A piece of code that you should run.", "type": "object", "properties": { "query": { "type": "string" }, "webhook": { "oneOf": [ { "type": "string" }, { "type": "object", "properties": { "url": { "type": "string" }, "headers": { "type": "object" } }, "required": ["url"] } ] } }, "required": ["query", "webhook"] } }, "properties": { "datamodel": { "description": "Type definitions for database models, relations, enums and other types", "type": ["string", "array"], "items": { "type": ["string", "array"] } }, "secret": { "description": "Secret for securing the API Endpoint", "type": "string", "items": { "type": "string" } }, "disableAuth": { "description": "Disable authentication for the endpoint", "type": "boolean", "items": { "type": "boolean" } }, "generate": { "type": "array", "items": { "type": "object", "properties": { "generator": { "type": "string" }, "output": { "type": "string" } } } }, "seed": { "description": "Database seed", "type": "object", "properties": { "import": { "type": "string" }, "run": { "type": "string" } } }, "subscriptions": { "description": "All server-side subscriptions", "type": "object", "additionalProperties": { "$ref": "#/definitions/subscription" } }, "custom": { "description": "Custom field to use in variable interpolations with \${self:custom.field}", "type": "object" }, "hooks": { "description": "Command hooks. Current available hooks are: post-deploy.", "type": "object" }, "endpoint": { "description": "Endpoint the service will be reachable at. This also determines the cluster the service will deployed to.", "type": "string", "items": { "type": "string" } }, "databaseType": { "type": "string", "oneOf": [{ "enum": ["relational", "document"] }] } }, "additionalProperties": false }`); class Output { log(...args) { console.log(args); } warn(...args) { console.warn(args); } getErrorPrefix(fileName, type = 'error') { return `[${type.toUpperCase()}] in ${fileName}: `; } } class Variables { constructor(fileName, options = {}, out = new Output(), envVars) { this.overwriteSyntax = /,/g; this.envRefSyntax = /^env:/g; this.selfRefSyntax = /^self:/g; this.stringRefSyntax = /('.*')|(".*")/g; this.optRefSyntax = /^opt:/g; // eslint-disable-next-line this.variableSyntax = new RegExp( // eslint-disable-next-line '\\${([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}', 'g'); this.out = out; this.fileName = fileName; this.options = options; this.envVars = envVars || process.env; } populateJson(json) { this.json = json; return this.populateObject(this.json).then(() => { return resolve(this.json); }); } populateObject(objectToPopulate) { const populateAll = []; const deepMapValues = (object, callback, propertyPath) => { const deepMapValuesIteratee = (value, key) => deepMapValues(value, callback, propertyPath ? propertyPath.concat(key) : [key]); if (isArray(object)) { return map(object, deepMapValuesIteratee); } else if (isObject(object) && !isDate(object) && !isFunction(object)) { return extend({}, object, mapValues(object, deepMapValuesIteratee)); } return callback(object, propertyPath); }; deepMapValues(objectToPopulate, (property, propertyPath) => { if (typeof property === 'string') { const populateSingleProperty = this.populateProperty(property, true) .then((newProperty) => set(objectToPopulate, propertyPath, newProperty)) .return(); populateAll.push(populateSingleProperty); } }); return all(populateAll).then(() => objectToPopulate); } populateProperty(propertyParam, populateInPlace) { let property = populateInPlace ? propertyParam : cloneDeep(propertyParam); const allValuesToPopulate = []; let warned = false; if (typeof property === 'string' && property.match(this.variableSyntax)) { property.match(this.variableSyntax).forEach(matchedString => { const variableString = matchedString .replace(this.variableSyntax, (_, varName) => varName.trim()) .replace(/\s/g, ''); let singleValueToPopulate = null; if (variableString.match(this.overwriteSyntax)) { singleValueToPopulate = this.overwrite(variableString); } else { singleValueToPopulate = this.getValueFromSource(variableString).then((valueToPopulate) => { if (typeof valueToPopulate === 'object') { return this.populateObject(valueToPopulate); } return valueToPopulate; }); } singleValueToPopulate = singleValueToPopulate.then(valueToPopulate => { if (this.warnIfNotFound(variableString, valueToPopulate)) { warned = true; } return this.populateVariable(property, matchedString, valueToPopulate).then((newProperty) => { property = newProperty; return resolve(property); }); }); allValuesToPopulate.push(singleValueToPopulate); }); return all(allValuesToPopulate).then(() => { if (property !== this.json && !warned) { return this.populateProperty(property); } return resolve(property); }); } return resolve(property); } populateVariable(propertyParam, matchedString, valueToPopulate) { let property = propertyParam; if (typeof valueToPopulate === 'string') { property = replaceall(matchedString, valueToPopulate, property); } else { if (property !== matchedString) { if (typeof valueToPopulate === 'number') { property = replaceall(matchedString, String(valueToPopulate), property); } else { const errorMessage = [ 'Trying to populate non string value into', ` a string for variable ${matchedString}.`, ' Please make sure the value of the property is a string.', ].join(''); this.out.warn(this.out.getErrorPrefix(this.fileName, 'warning') + errorMessage); } return resolve(property); } property = valueToPopulate; } return resolve(property); } overwrite(variableStringsString) { let finalValue; const variableStringsArray = variableStringsString.split(','); const allValuesFromSource = variableStringsArray.map((variableString) => this.getValueFromSource(variableString)); return all(allValuesFromSource).then((valuesFromSources) => { valuesFromSources.find((valueFromSource) => { finalValue = valueFromSource; return (finalValue !== null && typeof finalValue !== 'undefined' && !(typeof finalValue === 'object' && isEmpty(finalValue))); }); return resolve(finalValue); }); } getValueFromSource(variableString) { if (variableString.match(this.envRefSyntax)) { return this.getValueFromEnv(variableString); } else if (variableString.match(this.optRefSyntax)) { return this.getValueFromOptions(variableString); } else if (variableString.match(this.selfRefSyntax)) { return this.getValueFromSelf(variableString); } else if (variableString.match(this.stringRefSyntax)) { return this.getValueFromString(variableString); } const errorMessage = [ `Invalid variable reference syntax for variable ${variableString}.`, ' You can only reference env vars, options, & files.', ' You can check our docs for more info.', ].join(''); this.out.warn(this.out.getErrorPrefix(this.fileName, 'warning') + errorMessage); } getValueFromEnv(variableString) { const requestedEnvVar = variableString.split(':')[1]; const valueToPopulate = requestedEnvVar !== '' || '' in this.envVars ? this.envVars[requestedEnvVar] : this.envVars; return resolve(valueToPopulate); } getValueFromString(variableString) { const valueToPopulate = variableString.replace(/^['"]|['"]$/g, ''); return resolve(valueToPopulate); } getValueFromOptions(variableString) { const requestedOption = variableString.split(':')[1]; const valueToPopulate = requestedOption !== '' || '' in this.options ? this.options[requestedOption] : this.options; return resolve(valueToPopulate); } getValueFromSelf(variableString) { const valueToPopulate = this.json; const deepProperties = variableString.split(':')[1].split('.'); return this.getDeepValue(deepProperties, valueToPopulate); } getDeepValue(deepProperties, valueToPopulate) { return reduce(deepProperties, (computedValueToPopulateParam, subProperty) => { let computedValueToPopulate = computedValueToPopulateParam; if (typeof computedValueToPopulate === 'undefined') { computedValueToPopulate = {}; } else if (subProperty !== '' || '' in computedValueToPopulate) { computedValueToPopulate = computedValueToPopulate[subProperty]; } if (typeof computedValueToPopulate === 'string' && computedValueToPopulate.match(this.variableSyntax)) { return this.populateProperty(computedValueToPopulate); } return resolve(computedValueToPopulate); }, valueToPopulate); } warnIfNotFound(variableString, valueToPopulate) { if (valueToPopulate === null || typeof valueToPopulate === 'undefined' || (typeof valueToPopulate === 'object' && isEmpty(valueToPopulate))) { let varType; if (variableString.match(this.envRefSyntax)) { varType = 'environment variable'; } else if (variableString.match(this.optRefSyntax)) { varType = 'option'; } else if (variableString.match(this.selfRefSyntax)) { varType = 'self reference'; } this.out.warn(this.out.getErrorPrefix(this.fileName, 'warning') + `A valid ${varType} to satisfy the declaration '${variableString}' could not be found.`); return true; } return false; } } const debug$1 = require('debug')('yaml'); const ajv = new Ajv(); const validate = ajv.compile(schema); // this is used by the playground, which accepts additional properties const validateGraceful = ajv.compile({ ...schema, additionalProperties: true }); async function readDefinition(filePath, args, out = new Output(), envVars, graceful) { try { accessSync(filePath); } catch (_a) { throw new Error(`${filePath} could not be found.`); } const file = readFileSync(filePath, 'utf-8'); const json = safeLoad(file); // we need this copy because populateJson runs inplace const jsonCopy = { ...json }; const vars = new Variables(filePath, args, out, envVars); const populatedJson = await vars.populateJson(json); if (populatedJson.custom) { delete populatedJson.custom; } const valid = graceful ? validateGraceful(populatedJson) : validate(populatedJson); // TODO activate as soon as the backend sends valid yaml if (!valid) { const errorMessage = `Invalid prisma.yml file` + '\n' + printErrors(graceful ? validateGraceful.errors : validate.errors); throw new Error(errorMessage); } return { definition: populatedJson, rawJson: jsonCopy, }; } function printErrors(errors, name = 'prisma.yml') { return errors .map((e) => { const paramsKey = stringify(e.params); if (betterMessagesByParams[paramsKey]) { return betterMessagesByParams[paramsKey]; } const params = Object.keys(e.params) .map(key => `${key}: ${e.params[key]}`) .join(', '); debug$1(stringify(e.params)); return `${name}${e.dataPath} ${e.message}. ${params}`; }) .join('\n'); } const betterMessagesByParams = { // this is not up-to-date, stages are in again! // https://github.com/prisma/framework/issues/1461 '{"additionalProperty":"stages"}': 'prisma.yml should NOT have a "stages" property anymore. Stages are now just provided as CLI args.\nRead more here: https://goo.gl/SUD5i5', '{"additionalProperty":"types"}': 'prisma.yml should NOT have a "types" property anymore. It has been renamed to "datamodel"', }; /** * Comments out the current entry of a specific key in a yaml document and creates a new value next to it * @param key key in yaml document to comment out * @param newValue new value to add in the document */ function replaceYamlValue(input, key, newValue) { const ast = safeLoad$1(input); const position = getPosition(ast, key); const newEntry = `${key}: ${newValue}\n`; if (!position) { return input + '\n' + newEntry; } return (input.slice(0, position.start) + '#' + input.slice(position.start, position.end) + newEntry + input.slice(position.end)); } function getPosition(ast, key) { const mapping = ast.mappings.find((m) => m.key.value === key); if (!mapping) { return undefined; } return { start: mapping.startPosition, end: mapping.endPosition + 1, }; } class PrismaDefinitionClass { constructor(env, definitionPath, envVars = process.env, out) { this.secrets = null; this.definitionPath = definitionPath; if (definitionPath) { this.definitionDir = dirname(definitionPath); } this.env = env; this.out = out; this.envVars = envVars; } async load(args, envPath, graceful) { if (args.project) { const flagPath = resolve$1(args.project); try { accessSync(flagPath); } catch (_a) { throw new Error(`Prisma definition path specified by --project '${flagPath}' does not exist`); } this.definitionPath = flagPath; this.definitionDir = dirname(flagPath); await this.loadDefinition(args, graceful); this.validate(); return; } if (envPath) { try { accessSync(envPath); } catch (_b) { envPath = join(process.cwd(), envPath); } try { accessSync(envPath); } catch (_c) { throw new Error(`--env-file path '${envPath}' does not exist`); } } config({ path: envPath }); if (this.definitionPath) { await this.loadDefinition(args, graceful); this.validate(); } else { throw new Error(`Couldn’t find \`prisma.yml\` file. Are you in the right directory?`); } } async loadDefinition(args, graceful) { const { definition, rawJson } = await readDefinition(this.definitionPath, args, this.out, this.envVars, graceful); this.rawEndpoint = rawJson.endpoint; this.definition = definition; this.rawJson = rawJson; this.definitionString = readFileSync(this.definitionPath, 'utf-8'); this.typesString = this.getTypesString(this.definition); const secrets = this.definition.secret; this.secrets = secrets ? secrets.replace(/\s/g, '').split(',') : null; } get endpoint() { return (this.definition && this.definition.endpoint) || process.env.PRISMA_MANAGEMENT_API_ENDPOINT; } get clusterBaseUrl() { if (!this.definition || !this.endpoint) { return undefined; } const { clusterBaseUrl } = parseEndpoint(this.endpoint); return clusterBaseUrl; } get service() { if (!this.definition) { return undefined; } if (!this.endpoint) { return undefined; } const { service } = parseEndpoint(this.endpoint); return service; } get stage() { if (!this.definition) { return undefined; } if (!this.endpoint) { return undefined; } const { stage } = parseEndpoint(this.endpoint); return stage; } get cluster() { if (!this.definition) { return undefined; } if (!this.endpoint) { return undefined; } const { clusterName } = parseEndpoint(this.endpoint); return clusterName; } validate() { // shared clusters need a workspace const clusterName = this.getClusterName(); const cluster = this.env.clusterByName(clusterName); if (this.definition && clusterName && cluster && cluster.shared && !cluster.isPrivate && !this.getWorkspace() && clusterName !== 'shared-public-demo') { throw new Error(`Your \`cluster\` property in the prisma.yml is missing the workspace slug. Make sure that your \`cluster\` property looks like this: ${chalk.bold('/')}. You can also remove the cluster property from the prisma.yml and execute ${chalk.bold.green('prisma deploy')} again, to get that value auto-filled.`); } if (this.definition && this.definition.endpoint && clusterName && cluster && cluster.shared && !cluster.isPrivate && !this.getWorkspace() && clusterName !== 'shared-public-demo') { throw new Error(`The provided endpoint ${this.definition.endpoint} points to a demo cluster, but is missing the workspace slug. A valid demo endpoint looks like this: https://eu1.prisma.sh/myworkspace/service-name/stage-name`); } if (this.definition && this.definition.endpoint && !this.definition.endpoint.startsWith('http')) { throw new Error(`${chalk.bold(this.definition.endpoint)} is not a valid endpoint. It must start with http:// or https://`); } } getToken(serviceName, stageName) { if (this.secrets) { const data = { data: { service: `${serviceName}@${stageName}`, roles: ['admin'], }, }; return sign(data, this.secrets[0], { expiresIn: '7d', }); } return undefined; } async getCluster(_ = false) { if (this.definition && this.endpoint) { const clusterData = parseEndpoint(this.endpoint); const cluster = await this.getClusterByEndpoint(clusterData); this.env.removeCluster(clusterData.clusterName); this.env.addCluster(cluster); return cluster; } return undefined; } findClusterByBaseUrl(baseUrl) { return this.env.clusters.find(c => c.baseUrl.toLowerCase() === baseUrl); } async getClusterByEndpoint(data) { if (data.clusterBaseUrl && !process.env.PRISMA_MANAGEMENT_API_SECRET) { const cluster = this.findClusterByBaseUrl(data.clusterBaseUrl); if (cluster) { return cluster; } } const { clusterName, clusterBaseUrl, isPrivate, local, shared, workspaceSlug } = data; // if the cluster could potentially be served by the cloud api, fetch the available // clusters from the cloud api if (!local) { await this.env.fetchClusters(); const cluster = this.findClusterByBaseUrl(data.clusterBaseUrl); if (cluster) { return cluster; } } return new Cluster(this.out, clusterName, clusterBaseUrl, shared || isPrivate ? this.env.cloudSessionKey : undefined, local, shared, isPrivate, workspaceSlug); } getTypesString(definition) { const typesPaths = definition.datamodel ? Array.isArray(definition.datamodel) ? definition.datamodel : [definition.datamodel] : []; let allTypes = ''; typesPaths.forEach(unresolvedTypesPath => { const typesPath = join(this.definitionDir, unresolvedTypesPath); try { accessSync(typesPath); const types = readFileSync(typesPath, 'utf-8'); allTypes += types + '\n'; } catch (_a) { throw new Error(`The types definition file "${typesPath}" could not be found.`); } }); return allTypes; } getClusterName() { return this.cluster || null; } getWorkspace() { if (this.definition && this.endpoint) { const { workspaceSlug } = parseEndpoint(this.endpoint); if (workspaceSlug) { return workspaceSlug; } } return null; } async getDeployName() { const cluster = await this.getCluster(); return concatName(cluster, this.service, this.getWorkspace()); } getSubscriptions() { if (this.definition && this.definition.subscriptions) { return Object.entries(this.definition.subscriptions).map(([name, subscription]) => { const url = typeof subscription.webhook === 'string' ? subscription.webhook : subscription.webhook.url; const headers = typeof subscription.webhook === 'string' ? [] : transformHeaders(subscription.webhook.headers); let query = subscription.query; if (subscription.query.endsWith('.graphql')) { const queryPath = join(this.definitionDir, subscription.query); try { accessSync(queryPath); } catch (_a) { throw new Error(`Subscription query ${queryPath} provided in subscription "${name}" in prisma.yml does not exist.`); } query = readFileSync(queryPath, 'utf-8'); } return { name, query, headers, url, }; }); } return []; } replaceEndpoint(newEndpoint) { this.definitionString = replaceYamlValue(this.definitionString, 'endpoint', newEndpoint); writeFileSync(this.definitionPath, this.definitionString); } addDatamodel(datamodel) { this.definitionString += `\ndatamodel: ${datamodel}`; writeFileSync(this.definitionPath, this.definitionString); this.definition.datamodel = datamodel; } async getEndpoint(serviceInput, stageInput) { const cluster = await this.getCluster(); const service = serviceInput || this.service; const stage = stageInput || this.stage; const workspace = this.getWorkspace(); if (service && stage && cluster) { return cluster.getApiEndpoint(service, stage, workspace); } return null; } getHooks(hookType) { if (this.definition && this.definition.hooks && this.definition.hooks[hookType]) { const hooks = this.definition.hooks[hookType]; if (typeof hooks !== 'string' && !Array.isArray(hooks)) { throw new Error(`Hook ${hookType} provided in prisma.yml must be string or an array of strings.`); } return typeof hooks === 'string' ? [hooks] : hooks; } return []; } } function concatName(cluster, name, workspace) { if (cluster.shared) { const workspaceString = workspace ? `${workspace}~` : ''; return `${workspaceString}${name}`; } return name; } function transformHeaders(headers) { if (!headers) { return []; } return Object.entries(headers).map(([name, value]) => ({ name, value })); } class ClusterNotFound extends Error { constructor(name) { super(`Cluster '${name}' is neither a known shared cluster nor defined in your global .prismarc.`); } } class ClusterNotSet extends Error { constructor() { super(`No cluster set. In order to run this command, please set the "cluster" property in your prisma.yml`); } } const debug$2 = require('debug')('Environment'); class Environment { constructor(home, out = new Output(), version) { this.sharedClusters = ['prisma-eu1', 'prisma-us1']; this.clusterEndpointMap = clusterEndpointMap; this.globalRC = {}; this.clustersFetched = false; this.out = out; this.home = home; this.version = version; this.rcPath = join(this.home, '.prisma/config.yml'); mkdirSync(dirname(this.rcPath), { recursive: true }); } async load() { await this.loadGlobalRC(); } get cloudSessionKey() { return process.env.PRISMA_CLOUD_SESSION_KEY || this.globalRC.cloudSessionKey; } async renewToken() { if (this.cloudSessionKey) { const data = decode(this.cloudSessionKey); if (!data.exp) { return; } const timeLeft = data.exp * 1000 - Date.now(); if (timeLeft < 1000 * 60 * 60 * 24 && timeLeft > 0) { try { const res = await this.requestCloudApi(` mutation { renewToken } `); if (res.renewToken) { this.globalRC.cloudSessionKey = res.renewToken; this.saveGlobalRC(); } } catch (e) { debug$2(e); } } } } async fetchClusters() { if (!this.clustersFetched && this.cloudSessionKey) { const renewPromise = this.renewToken(); try { const res = (await Promise.race([ this.requestCloudApi(` query prismaCliGetClusters { me { memberships { workspace { id slug clusters { id name connectInfo { endpoint } customConnectionInfo { endpoint } } } } } } `), // eslint-disable-next-line new Promise((_, r) => setTimeout(() => r(), 6000)), ])); if (!res) { return; } if (res.me && res.me.memberships && Array.isArray(res.me.memberships)) { // clean up all prisma-eu1 and prisma-us1 clusters if they already exist this.clusters = this.clusters.filter(c => c.name !== 'prisma-eu1' && c.name !== 'prisma-us1'); res.me.memberships.forEach((m) => { m.workspace.clusters.forEach((cluster) => { const endpoint = cluster.connectInfo ? cluster.connectInfo.endpoint : cluster.customConnectionInfo ? cluster.customConnectionInfo.endpoint : this.clusterEndpointMap[cluster.name]; this.addCluster(new Cluster(this.out, cluster.name, endpoint, this.globalRC.cloudSessionKey, false, ['prisma-eu1', 'prisma-us1'].includes(cluster.name), !['prisma-eu1', 'prisma-us1'].includes(cluster.name), m.workspace.slug)); }); }); } } catch (e) { debug$2(e); } await renewPromise; } } clusterByName(name, throws = false) { if (!this.clusters) { return; } const cluster = this.clusters.find(c => c.name === name); if (!throws) { return cluster; } if (!cluster) { if (!name) { throw new ClusterNotSet(); } throw new ClusterNotFound(name); } return cluster; } setToken(token) { this.globalRC.cloudSessionKey = token; } addCluster(cluster) { const existingClusterIndex = this.clusters.findIndex(c => { if (cluster.workspaceSlug) { return c.workspaceSlug === cluster.workspaceSlug && c.name === cluster.name; } else { return c.name === cluster.name; } }); if (existingClusterIndex > -1) { this.clusters.splice(existingClusterIndex, 1); } this.clusters.push(cluster); } removeCluster(name) { this.clusters = this.clusters.filter(c => c.name !== name); } saveGlobalRC() { const rc = { cloudSessionKey: this.globalRC.cloudSessionKey ? this.globalRC.cloudSessionKey.trim() : undefined, clusters: this.getLocalClusterConfig(), }; // parse & stringify to rm undefined for yaml parser const rcString = safeDump(JSON.parse(JSON.stringify(rc))); writeFileSync(this.rcPath, rcString); } setActiveCluster(cluster) { this.activeCluster = cluster; } async loadGlobalRC() { if (this.rcPath) { try { accessSync(this.rcPath); const globalFile = readFileSync(this.rcPath, 'utf-8'); await this.parseGlobalRC(globalFile); } catch (_a) { await this.parseGlobalRC(); } } else { await this.parseGlobalRC(); } } async parseGlobalRC(globalFile) { if (globalFile) { this.globalRC = await this.loadYaml(globalFile, this.rcPath); } this.clusters = this.initClusters(this.globalRC); } async loadYaml(file, filePath = null) { if (file) { let content; try { content = safeLoad(file); } catch (e) { throw new Error(`Yaml parsing error in ${filePath}: ${e.message}`); } const variables = new Variables(filePath || 'no filepath provided', this.args, this.out); content = await variables.populateJson(content); return content; } else { return {}; } } initClusters(rc) { const sharedClusters = this.getSharedClusters(rc); return [...sharedClusters]; } getSharedClusters(rc) { return this.sharedClusters.map(clusterName => { return new Cluster(this.out, clusterName, this.clusterEndpointMap[clusterName], rc && rc.cloudSessionKey, false, true); }); } getLocalClusterConfig() { return this.clusters .filter(c => !c.shared && c.clusterSecret !== this.cloudSessionKey && !c.isPrivate) .reduce((acc, cluster) => { return { ...acc, [cluster.name]: { host: cluster.baseUrl, clusterSecret: cluster.clusterSecret, }, }; }, {}); } async requestCloudApi(query) { const res = await fetch('https://api.cloud.prisma.sh', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cloudSessionKey}`, 'X-Cli-Version': this.version, }, body: JSON.stringify({ query, }), proxy: getProxyAgent('https://api.cloud.prisma.sh'), }); const json = await res.json(); return json.data; } } const { access } = promises; /** * This loader loads a schema from a `prisma.yml` file */ class PrismaLoader extends UrlLoader { loaderId() { return 'prisma'; } async canLoad(prismaConfigFilePath, options) { if (typeof prismaConfigFilePath === 'string' && prismaConfigFilePath.endsWith('prisma.yml')) { const joinedYmlPath = join(options.cwd || cwd(), prismaConfigFilePath); try { await access(joinedYmlPath); return true; } catch (_a) { return false; } } return false; } async load(prismaConfigFilePath, options) { const { graceful, envVars } = options; const home = homedir(); const env = new Environment(home); await env.load(); const joinedYmlPath = join(options.cwd || cwd(), prismaConfigFilePath); const definition = new PrismaDefinitionClass(env, joinedYmlPath, envVars); await definition.load({}, undefined, graceful); const serviceName = definition.service; const stage = definition.stage; const clusterName = definition.cluster; if (!clusterName) { throw new Error(`No cluster set. Please set the "cluster" property in your prisma.yml`); } const cluster = await definition.getCluster(); if (!cluster) { throw new Error(`Cluster ${clusterName} provided in prisma.yml could not be found in global ~/.prisma/config.yml. Please check in ~/.prisma/config.yml, if the cluster exists. You can use \`docker-compose up -d\` to start a new cluster.`); } const token = definition.getToken(serviceName, stage); const url = cluster.getApiEndpoint(serviceName, stage, definition.getWorkspace() || undefined); const headers = token ? { Authorization: `Bearer ${token}`, } : undefined; return super.load(url, { headers }); } } export { PrismaLoader }; //# sourceMappingURL=index.esm.js.map