@gfdelarue/testkit 0.3.3 → 0.3.4
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/README.md +7 -0
- package/dist/config/loader/validation.js +1 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/process/execa-bundle.json +1 -1
- package/dist/steps/environment.d.ts +6 -0
- package/dist/steps/environment.js +1 -1
- package/dist/steps/index.d.ts +1 -1
- package/dist/steps/index.js +1 -1
- package/package.json +1 -1
- package/testkit-config-schema.md +52 -0
package/README.md
CHANGED
|
@@ -1068,6 +1068,13 @@ platform's host address.
|
|
|
1068
1068
|
- template fingerprints are derived automatically from env files, source schema
|
|
1069
1069
|
cache, migrate/seed config, and repo contents
|
|
1070
1070
|
|
|
1071
|
+
The service process receives its provisioned database as `DATABASE_URL`. Tasks
|
|
1072
|
+
get it only through declared `env` values: use the `{dbUrl}`, `{dbHost}`,
|
|
1073
|
+
`{dbPort}`, `{dbName}`, `{dbUser}`, `{dbPassword}`, and `{dbSsl}` tokens, with
|
|
1074
|
+
`:serviceName` to read the database of a service listed in `dependsOn`. Testkit
|
|
1075
|
+
resolves them from the database provisioned for the current run. They are
|
|
1076
|
+
env-only; see [`env`](testkit-config-schema.md#env).
|
|
1077
|
+
|
|
1071
1078
|
`db schema refresh` forces a source database dump into the `.testkit` source
|
|
1072
1079
|
schema cache. `db schema verify` prepares local templates and verifies local
|
|
1073
1080
|
replay against the cached/refreshed source schema. `--skip-schema-source-verify`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateConfiguredCollection as
|
|
1
|
+
import{finalizeConfiguredInputs as l,finalizeConfiguredSteps as w,validateConfiguredCollection as d}from"../../steps/configured-steps.js";import{findDatabasePlaceholders as h}from"../../steps/environment.js";import{resolveEngineProfile as S}from"../../engines/catalog.js";import{ensureExistingContainedPath as g,ensureExistingPath as E}from"./paths.js";const m=["migrate","seed","verify"];export function validateConfigCoverage(e){const i=new Set(e.map(r=>r.name));for(const r of e){for(const t of r.testkit.dependsOn)if(!i.has(t))throw new Error(`Service "${r.name}" depends on "${t}", but ${t} is not defined`);const n=r.testkit.databaseFrom;if(n&&!i.has(n))throw new Error(`Service "${r.name}" databaseFrom "${n}" is not defined`)}}export function validateServiceConfig({name:e,local:i,database:r,databaseFrom:n,runtime:t,dependsOn:c,suites:p,productDir:s,requireLocalRuntime:v=!0}){const $=Object.entries(p).some(([a,u])=>(u||[]).some(o=>{if(o.testType!==a)throw new Error(`Service "${e}" suite ${a}:${o.name} declares conflicting test type ${o.testType}`);const f=S(o.testType,o.engineId);if(f.id!==o.profileId)throw new Error(`Service "${e}" suite ${o.testType}:${o.name} declares conflicting engine profile ${o.profileId}; expected ${f.id}`);return f.capabilities.applicationRuntime==="required"}));if(v&&$&&!i)throw new Error(`Service "${e}" defines suites whose engine profile requires an application runtime, but no local runtime could be resolved. Add it in testkit.config.ts.`);if(r&&n)throw new Error(`Service "${e}" cannot define both database and databaseFrom`);if(t.instances<1)throw new Error(`Service "${e}" runtime.instances must be a positive integer`);if(t.maxConcurrentTasks<=0)throw new Error(`Service "${e}" runtime.maxConcurrentTasks must be a positive integer when provided`);for(const a of c)if(a===e)throw new Error(`Service "${e}" cannot depend on itself`);if(b({name:e,local:i,database:r,runtime:t}),i?.cwd&&E(s,i.cwd,`Service "${e}" local.cwd`),t.toolchain?.cwd&&g(s,t.toolchain.cwd,`Service "${e}" runtime.toolchain.cwd`),r)for(const a of m)d({productDir:s,label:`Service "${e}" database.template.${a}`,steps:r.template[a]});d({productDir:s,label:`Service "${e}" database.template`,inputs:r?.template.inputs||[]}),d({productDir:s,label:`Service "${e}" runtime.prepare`,inputs:t.prepare.inputs,steps:t.prepare.steps})}function b({name:e,local:i,database:r,runtime:n}){const t=c=>p=>{const s=h(p).at(0);if(s)throw new Error(`Service "${e}" ${c} uses ${s.placeholder}. Database placeholders resolve only in env values; commands read DATABASE_URL or a service env entry at run time.`);return p};if(i&&(t("local.start")(i.start),t("local.cwd")(i.cwd)),l(n.prepare.inputs,t("runtime.prepare.inputs")),w(n.prepare.steps,t("runtime.prepare.steps")),r){l(r.template.inputs,t("database.template.inputs"));for(const c of m)w(r.template[c],t(`database.template.${c}`))}}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import o from"fs";import s from"path";import{createRequire as J}from"node:module";import{collectRepoState as W,summarizeRepoStateForMetadata as C}from"../repo/state.js";import{
|
|
2
|
-
`))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const a=t?.cachePath||await getSourceSchemaCachePath(r),n=t?.active&&t.cacheKey?t.cacheKey:s.relative(getSourceSchemaRootDir(r),s.dirname(a));if(e.update(`source-schema-key:${n}`),e.update(`source-schema-cache:${s.relative(r.productDir,a)}`),!o.existsSync(a)){e.update(":missing");return}const i=o.statSync(a);e.update(`:${i.size.toString()}:${i.mtimeMs.toString()}`),e.update(o.readFileSync(a))}async function F(e,r,t={}){const c=t.setupRegistry,a=c?.start({config:e,stage:"source-schema:refresh",kind:"database-source-schema",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`});try{const n=await re(
|
|
3
|
-
`}function
|
|
1
|
+
import o from"fs";import s from"path";import{createRequire as J}from"node:module";import{collectRepoState as W,summarizeRepoStateForMetadata as C}from"../repo/state.js";import{findDatabasePlaceholders as V,resolveExecutionEnvValues as X}from"../steps/index.js";import{dumpPostgresSchemaToFile as Z,captureTemplateSnapshotText as G,resolvePgDumpClientImage as Q,runTemplateStep as Y,sanitizeSnapshotText as ee}from"./template-steps.js";import{getSourceSchemaRefreshLockPath as re,withSourceSchemaRefreshLock as te}from"./source-refresh-lock.js";import{resolveSourceSchemaDumpUrl as ae}from"./source-url.js";import{makeTempDir as ce,removeTempDir as ne}from"../process/files/temp-dir.js";import{requireContainedDatabaseStateDirectory as R,resolveContainedDatabaseStateDirectory as w}from"./state.js";const k=J(import.meta.url);let D;function se(){if(D)return D;const e=k.resolve("diff/package.json"),t=JSON.parse(o.readFileSync(e,"utf8"))?.exports?.["."]?.import?.default;if(typeof t!="string"||!t.startsWith("./"))throw new Error("The diff package must declare a local ESM import entry");const c=s.dirname(e),a=s.resolve(c,t),n=s.relative(c,a);if(!n||n===".."||n.startsWith(`..${s.sep}`)||s.isAbsolute(n))throw new Error("The diff package ESM import entry must stay within its package directory");return D=k(a)}const K="db",U="source-schemas",P="source-schema.sql",E="source-schema.meta.json",ie="index.json",$=20,p=String;export function getSourceSchemaRootDir(e){return w(e.productDir,K,e.name,U)}export async function getSourceSchemaCachePath(e,r={}){return(await resolveSourceSchemaCacheLocation(e,r)).cachePath}export async function resolveSourceSchemaCacheLocation(e,r={}){const t=r.repoState||await W(e.productDir,{fingerprints:e.testkit?.fingerprints}),c=getSourceSchemaRootDir(e),a=w(e.productDir,K,e.name,U,...t.cacheKey.split("/").map(v));return{rootDir:c,cacheDir:a,cachePath:s.join(a,P),metadataPath:s.join(a,E),indexPath:s.join(c,ie),cacheKey:t.cacheKey,cacheKind:t.kind,repoState:t}}export async function resolveSourceSchemaState(e,r={}){const t=e.testkit.database?.sourceSchema||null;if(!t)return{active:!1};const c=await resolveSourceSchemaCacheLocation(e,r),a=c.cachePath,n=o.existsSync(a);n&&A(a);const i=r.env||process.env,u=t.kind==="env"?[t.env]:[T(e.name),"TESTKIT_SCHEMA_SOURCE_DATABASE_URL"];ge(e,u);const h=X(e,[...u,"CI"],i),m=u.find(z=>{const _=h[z];return typeof _=="string"&&_.trim().length>0})||null,S=m?h[m]:void 0,f=typeof S=="string"?S.trim():null;return t.kind==="env"||!!f||n?{active:!0,cachePath:a,metadataPath:c.metadataPath,indexPath:c.indexPath,rootDir:c.rootDir,cacheDir:c.cacheDir,cacheKey:c.cacheKey,cacheKind:c.cacheKind,repoState:c.repoState,envName:m,sourceUrl:f,sourceSchema:t,cacheExists:n,refreshed:!1,ci:h.CI==="true",unavailableMode:be(t.unavailable,i)}:{active:!1,cachePath:a}}export async function prepareSourceSchemaCache(e,r={}){r.signal?.throwIfAborted();const t=await resolveSourceSchemaState(e,r);if(!t.active)return t;if(H(t)){if(fe(t))try{const a=await F(e,t,r),n={...t,refreshed:!0,cacheExists:!0,refreshInfo:a};return y(e,n,{refreshed:!0}),n}catch(a){r.signal?.throwIfAborted();const n=oe(e,t,a,r);if(n)return n;throw a}return y(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source schema cache hit"}),t}if(t.cacheExists&&t.unavailableMode==="warn-cache")return y(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source unavailable; using cached source schema"}),t;const c=t.sourceSchema.kind==="env"?t.sourceSchema.env:`${T(e.name)} or TESTKIT_SCHEMA_SOURCE_DATABASE_URL`;throw new Error(`Source schema database URL is unavailable for service "${e.name}". Set ${c}.`)}function oe(e,r,t,c={}){if(r.unavailableMode!=="warn-cache")return null;const a=ue(e,r);if(!a)return null;const n={...r,...a,cacheExists:!0,refreshed:!1,refreshError:xe(t)};return c.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:`source unavailable; using cached source schema: ${s.relative(e.productDir,a.cachePath)}`}),n}function ue(e,r){const t=M(e,r,{cachePath:r.cachePath,metadataPath:r.metadataPath,cacheKey:r.cacheKey,cacheKind:r.cacheKind});if(t)return t;const c=j(r.indexPath);c.sort((a,n)=>Date.parse(n.lastUsedAt||n.refreshedAt||"0")-Date.parse(a.lastUsedAt||a.refreshedAt||"0"));for(const a of c){if(!a.cachePath)continue;const n=s.resolve(e.productDir,a.cachePath),i=M(e,r,{cachePath:n,metadataPath:s.join(s.dirname(n),E),cacheKey:a.cacheKey,cacheKind:a.kind||"unknown"});if(i)return i}return null}function M(e,r,t){const c=s.resolve(t.cachePath);if(s.basename(c)!==P)return null;const a=R(e.productDir,s.dirname(c),`Service "${e.name}" source schema cache directory`),n=s.join(a,P),i=s.resolve(r.rootDir);if(n!==i&&!n.startsWith(`${i}${s.sep}`)||!o.existsSync(n))return null;const u=o.lstatSync(n);if(u.isSymbolicLink()||!u.isFile())throw new Error(`Refusing unsafe source schema cache file at ${n}`);return{cachePath:n,metadataPath:s.join(a,E),cacheDir:a,cacheKey:t.cacheKey,cacheKind:t.cacheKind,indexPath:r.indexPath,rootDir:r.rootDir}}export async function forceRefreshSourceSchemaCache(e,r,t={}){const c=await resolveSourceSchemaState(e,t);if(!c.active||!H(c)){const i=r?.active?r.envName:null,u=r?.active&&r.sourceSchema.kind==="env"?r.sourceSchema.env:null,h=i||u||"source schema env";throw new Error(`Cannot refresh source schema for service "${e.name}" because ${h} is unavailable.`)}const a=await F(e,c,t),n={...c,refreshed:!0,cacheExists:!0,refreshInfo:a};return y(e,n,{refreshed:!0}),n}export async function applySourceSchemaCache(e,r,t,c={}){if(!t?.active)return!1;if(!o.existsSync(t.cachePath))throw new Error(`Missing source schema cache for service "${e.name}": ${t.cachePath}`);const a={kind:"sql-file",path:s.relative(e.productDir,t.cachePath).split(s.sep).join("/"),cwd:null,inputs:[]};return await Y(e,"source-schema",a,0,r,c),!0}export async function verifyLocalSchemaMatchesSource(e,r,t,c={}){if(!t?.active)return{status:"disabled"};if(!t.sourceSchema.verify||c.skipSchemaSourceVerify)return c.setupRegistry?.recordCached({config:e,stage:"source-schema:verify",kind:"database-source-schema",summary:"source schema verification skipped"}),{status:"skipped"};ve(e);const a=de(t.cachePath),n=await G(e,r,{image:await c.resolveLocalSnapshotImage?.(),ownedContainer:c.ownedContainer||null,reporter:c.reporter||null,signal:c.signal});return n===a?{status:"matched"}:{status:"mismatch",diagnostics:await De(e,t,a,n)}}export function createSourceSchemaMismatchError(e,r,t){const c=[`Local schema replay does not match source schema for service "${e.name}".`,`Source cache: ${s.relative(e.productDir,r.cachePath)}`];return t?.status==="mismatch"&&c.push(`Diff: ${s.relative(e.productDir,t.diagnostics.diffPath)}`),new Error(c.join(`
|
|
2
|
+
`))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const a=t?.cachePath||await getSourceSchemaCachePath(r),n=t?.active&&t.cacheKey?t.cacheKey:s.relative(getSourceSchemaRootDir(r),s.dirname(a));if(e.update(`source-schema-key:${n}`),e.update(`source-schema-cache:${s.relative(r.productDir,a)}`),!o.existsSync(a)){e.update(":missing");return}const i=o.statSync(a);e.update(`:${i.size.toString()}:${i.mtimeMs.toString()}`),e.update(o.readFileSync(a))}async function F(e,r,t={}){const c=t.setupRegistry,a=c?.start({config:e,stage:"source-schema:refresh",kind:"database-source-schema",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`});try{const n=await te(re(r.cachePath),async({waited:u,requestedAt:h})=>{const m=le(r,h);if(m)return{reusedExistingRefresh:!0,waitedForLock:u,metadata:m};const S=await he(e,r.sourceUrl,r.cachePath,{reporter:t.reporter||null,logRecord:a?._logRecord||null,env:t.env||process.env,signal:t.signal}),f=me(e,r,S);return pe(r.metadataPath,f),{reusedExistingRefresh:!1,waitedForLock:u,metadata:f}},{signal:t.signal}),i=a&&c?c.finish(a,{status:"passed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`}):null;return i&&t.reporter?.setupOperationFinished?.(i),n}catch(n){const i=a?c?.finish(a,{status:"failed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`,error:_e(n)}):null;throw i&&t.reporter?.setupOperationFinished?.(i),n}}async function he(e,r,t,c={}){const a=ae(r);o.mkdirSync(s.dirname(t),{recursive:!0});const n=ce(".source-schema-",{parentDir:s.dirname(t)}),i=s.join(n,s.basename(t));try{const u=await Q(e,a.dumpUrl,{env:c.env||process.env,pgApplicationName:b(e.name),signal:c.signal});return await Z(e,i,a.dumpUrl,{...c,image:u,pgApplicationName:b(e.name)}),o.renameSync(i,t),{resolution:a,pgDump:{args:["--schema-only","--no-owner","--no-privileges"],applicationName:b(e.name),image:u}}}finally{ne(n)}}function me(e,r,t){return{refreshedAt:new Date().toISOString(),serviceName:e.name,envName:r.envName,cacheKey:r.cacheKey,cacheKind:r.cacheKind,cachePath:s.relative(e.productDir,r.cachePath),repo:C(r.repoState),sourceUrl:t.resolution.metadata,pgDump:t.pgDump}}function le(e,r){if(!o.existsSync(e.cachePath))return null;const t=g(e.metadataPath);if(!ke(t))return null;const c=Date.parse(t.refreshedAt);return!Number.isFinite(c)||c<r?null:t}function b(e){return`testkit:schema-source:${e.replace(/[^a-zA-Z0-9._:-]+/g,"-").slice(0,24)||"service"}`}function de(e){return ee(o.readFileSync(e,"utf8"))}function fe(e){if(!e.cacheExists)return!0;if(e.sourceSchema.refresh.mode==="auto")return e.ci;if(e.sourceSchema.refresh.mode==="ttl"){const r=x(e.metadataPath),t=r?Date.parse(r):0;return!Number.isFinite(t)||t<=0?!0:Date.now()-t>=e.sourceSchema.refresh.ttlSeconds*1e3}return!0}function y(e,r,t={}){if(!r?.active)return;const c=new Date().toISOString(),a=j(r.indexPath).filter(u=>u.cacheKey!==r.cacheKey),n=x(r.metadataPath);a.push({cacheKey:r.cacheKey,kind:r.cacheKind,commitSha:r.repoState.commitSha||null,branch:r.repoState.branch||null,detached:r.repoState.detached,dirty:r.repoState.dirty,dirtyFingerprint:r.repoState.dirtyFingerprint||null,contentFingerprint:r.repoState.contentFingerprint||null,remoteUrl:r.repoState.remoteUrl||null,repository:r.repoState.repository,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t.refreshed?c:n,lastUsedAt:c});const i=Se(e,r,a);N(r.indexPath,{version:1,entries:i})}function Se(e,r,t){if(t.length<=$)return t;const c=[...t].sort((n,i)=>Date.parse(i.lastUsedAt||"0")-Date.parse(n.lastUsedAt||"0")),a=new Set(c.slice(0,$).map(n=>n.cacheKey));a.add(r.cacheKey);for(const n of t){if(a.has(n.cacheKey))continue;const i=n.cachePath?s.join(e.productDir,n.cachePath):null,u=i?s.resolve(i):null,h=s.resolve(r.rootDir);if(!i||!u||u!==h&&!u.startsWith(`${h}${s.sep}`))continue;const m=R(e.productDir,s.dirname(i),`Service "${e.name}" pruned source schema cache directory`);o.rmSync(m,{recursive:!0,force:!0})}return t.filter(n=>a.has(n.cacheKey))}function pe(e,r){I(e,r)}function N(e,r){I(e,r)}function I(e,r){o.mkdirSync(s.dirname(e),{recursive:!0}),A(e),o.writeFileSync(e,Ae(r))}function g(e){if(!e||!o.existsSync(e))return null;A(e);try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function L(e){return w(e.productDir,"results","schema")}function A(e){if(!o.existsSync(e))return;const r=o.lstatSync(e);if(r.isSymbolicLink()||!r.isFile())throw new Error(`Refusing unsafe source schema state file at ${e}`)}function ye(e){return`${v(e.runtimeLabel||e.name)}__${v(e.name)}`}function ve(e){const r=L(e);if(!o.existsSync(r))return;const t=v(e.name);for(const c of o.readdirSync(r))we(c,t)&&o.rmSync(s.join(r,c),{force:!0})}function we(e,r){for(const t of["__source-schema.sql","__local-replay-schema.sql","__schema.diff","__schema.meta.json"])if(e.endsWith(t))return e.slice(0,-t.length).endsWith(`__${r}`);return!1}async function De(e,r,t,c){const a=L(e);o.mkdirSync(a,{recursive:!0});const n=ye(e),i=s.join(a,`${n}__source-schema.sql`),u=s.join(a,`${n}__local-replay-schema.sql`),h=s.join(a,`${n}__schema.diff`),m=s.join(a,`${n}__schema.meta.json`);return o.writeFileSync(i,t),o.writeFileSync(u,c),o.writeFileSync(h,await Ee(i,u)),N(m,Pe(e,r)),{sourceCachePath:r.cachePath,sourcePath:i,localPath:u,diffPath:h,metaPath:m}}function Pe(e,r){const t=x(r.metadataPath);return{version:1,createdAt:new Date().toISOString(),serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,sourceSchema:{envName:r.envName||null,cacheKey:r.cacheKey||null,cacheKind:r.cacheKind||null,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t},repo:C(r.repoState)}}function Ee(e,r){return new Promise(t=>{t(buildUnifiedDiffText(o.readFileSync(e,"utf8"),o.readFileSync(r,"utf8"),e,r))})}export function buildUnifiedDiffText(e,r,t="source",c="target"){if(e===r)return"";const{createTwoFilesPatch:a}=se();return a(t,c,p(e),p(r),"","",{context:3})}function be(e,r){return e==="fail"||e==="warn-cache"?e:r.CI==="true"?"fail":"warn-cache"}function T(e){return`TESTKIT_${e.toUpperCase().replace(/[^A-Z0-9]+/g,"_")}_SCHEMA_SOURCE_DATABASE_URL`}function ge(e,r){const t={...e.testkit.serviceEnv,...e.testkit.local?.env};for(const c of r){const a=t[c];if(typeof a!="string")continue;const n=V(a).find(i=>(i.serviceName??e.name)===e.name);if(n)throw new Error(`Service "${e.name}" env ${c} uses ${n.placeholder}, which is the database the source schema verifies. Point it at another service's database or an external database URL.`)}}function v(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"schema"}function j(e){const r=g(e);return!d(r)||!Array.isArray(r.entries)?[]:r.entries.filter(Ce)}function x(e){const r=g(e);return d(r)&&typeof r.refreshedAt=="string"?r.refreshedAt:null}function Ae(e){return`${p(JSON.stringify(e,null,2))}
|
|
3
|
+
`}function xe(e){const r=O(e,"message");return!!r?r:p(e)}function _e(e){const r=O(e,"message");return!!r?r:e}function O(e,r){if(!(e===null||typeof e!="object"&&typeof e!="function"))return Reflect.get(e,r)}function H(e){return!!e.sourceUrl}function Ce(e){return!d(e)||typeof e.cacheKey!="string"||e.cacheKey.length===0?!1:l(e.branch)&&l(e.cachePath)&&l(e.commitSha)&&l(e.contentFingerprint)&&B(e.detached)&&B(e.dirty)&&l(e.dirtyFingerprint)&&l(e.kind)&&l(e.lastUsedAt)&&l(e.refreshedAt)&&l(e.remoteUrl)&&Re(e.repository)}function Re(e){return e==null||d(e)&&typeof e.host=="string"&&typeof e.owner=="string"&&typeof e.name=="string"}function ke(e){return d(e)&&typeof e.refreshedAt=="string"&&(e.sourceUrl===void 0||Ke(e.sourceUrl))}function Ke(e){return d(e)&&typeof e.originalHost=="string"&&q(e.originalClassification)&&typeof e.resolvedHost=="string"&&q(e.resolvedClassification)&&typeof e.rewritten=="boolean"}function q(e){return e==="neon-pooler"||e==="neon-direct"||e==="unsupported-pooler"||e==="unknown-direct"}function Ue(e){return e===null||typeof e=="string"}function l(e){return e===void 0||Ue(e)}function B(e){return e===void 0||typeof e=="boolean"}function d(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"entrypoint": "dist/process/process.js",
|
|
4
4
|
"sourceSha256": "a54f01864b603cee568b2945390dff35d578e07f6d26b5add06014169c3d17b0",
|
|
5
5
|
"bundleSha256": "62f76cf9fd019f6c552bfde2787f058917caf279670a62e7a6bfa12be34cb4b1",
|
|
6
|
-
"packageLockSha256": "
|
|
6
|
+
"packageLockSha256": "59332a4b9bf51d0bedee38025690d15fdf490151211ea06c4109f0896796709f",
|
|
7
7
|
"esbuildVersion": "0.28.1",
|
|
8
8
|
"options": {
|
|
9
9
|
"format": "esm",
|
|
@@ -21,6 +21,10 @@ export interface DatabaseTemplateBindings {
|
|
|
21
21
|
user: string;
|
|
22
22
|
}
|
|
23
23
|
export type DatabaseTemplateBindingsReader = (stateDir: string) => DatabaseTemplateBindings | null;
|
|
24
|
+
export interface DatabasePlaceholder {
|
|
25
|
+
placeholder: string;
|
|
26
|
+
serviceName: null | string;
|
|
27
|
+
}
|
|
24
28
|
export interface ExecutionTemplateContextValues {
|
|
25
29
|
[key: string]: unknown;
|
|
26
30
|
baseUrlByService?: StringMap;
|
|
@@ -70,9 +74,11 @@ export interface TemplateResolutionContext extends ExecutionTemplateContextValue
|
|
|
70
74
|
export declare function buildExecutionEnv(config: Readonly<ExecutionEnvironmentConfig>, extraEnv?: Readonly<NodeJS.ProcessEnv> | null, processEnv?: Readonly<NodeJS.ProcessEnv> | null, options?: Readonly<BuildExecutionEnvironmentOptions>): NodeJS.ProcessEnv;
|
|
71
75
|
export declare function buildTemplateExecutionEnv(config: Readonly<ExecutionEnvironmentConfig>, extraEnv?: Readonly<NodeJS.ProcessEnv> | null, processEnv?: Readonly<NodeJS.ProcessEnv> | null, options?: Readonly<BuildExecutionEnvironmentOptions>): NodeJS.ProcessEnv;
|
|
72
76
|
export declare function buildTaskExecutionEnv(config: Readonly<ExecutionEnvironmentConfig>, lease: Readonly<ExecutionEnvironmentLease> | null, extraEnv?: Readonly<NodeJS.ProcessEnv> | null, processEnv?: Readonly<NodeJS.ProcessEnv> | null, options?: Readonly<BuildExecutionEnvironmentOptions>): NodeJS.ProcessEnv;
|
|
77
|
+
export declare function resolveExecutionEnvValues(config: Readonly<ExecutionEnvironmentConfig>, names: readonly string[], processEnv?: Readonly<NodeJS.ProcessEnv> | null): NodeJS.ProcessEnv;
|
|
73
78
|
export declare function buildTemplateContext(config: Readonly<ExecutionEnvironmentConfig>, lease: Readonly<ExecutionEnvironmentLease> | null): TemplateResolutionContext;
|
|
74
79
|
export declare function finalizeString(value: string, context: TemplateResolutionContext): string;
|
|
75
80
|
export declare function finalizeSerializable(value: unknown, context: TemplateResolutionContext): unknown;
|
|
76
81
|
export declare function resolveTemplateString(value: string, context: TemplateResolutionContext): string;
|
|
77
82
|
export declare function resolveTemplateString(value: unknown, context: TemplateResolutionContext): unknown;
|
|
83
|
+
export declare function findDatabasePlaceholders(value: string): DatabasePlaceholder[];
|
|
78
84
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import v from"path";import{joinPathList as m,packageBinPath as E,shellQuote as w}from"../process/host/platform.js";export function buildExecutionEnv(r,e={},t=process.env,n={}){return o(r,null,e,t,n)}export function buildTemplateExecutionEnv(r,e={},t=process.env,n={}){return o(r,null,e,t,{...n,omitRuntimeDatabaseBindings:!0})}export function buildTaskExecutionEnv(r,e,t={},n=process.env,i={}){return o(r,e,t,n,i)}export function resolveExecutionEnvValues(r,e,t=process.env){return o(r,null,{},t,{includeTestkitControls:!1},new Set(e))}function o(r,e,t,n,i={},s=null){const a=c(i.inheritProcessEnv===!1?{}:n,s,!1);delete a.DATABASE_URL;const u=buildTemplateContext(r,e),p=i.omitRuntimeDatabaseBindings===!0,f=c(r.testkit.serviceEnv,s,p),b=c(r.testkit.local?.env,s,p);return{...a,...d(f,u),...d(b,u),...d(c(t,s,!1),u),...i.includeTestkitControls===!1?{}:{TESTKIT_ACTIVE:"1",TESTKIT_MODE:i.mode||r.testkit.mode||"test",...r.runtimeId?{TESTKIT_RUNTIME_ID:String(r.runtimeId)}:{},...e?.leaseId?{TESTKIT_LEASE_ID:String(e.leaseId)}:{},...e?.leaseDir?{TESTKIT_LEASE_DIR:e.leaseDir}:{}}}}export function buildTemplateContext(r,e){const t=r.testkit.templateContext||{};return{...t,runtimeId:r.runtimeId||t.runtimeId||null,runtimeLabel:r.runtimeLabel||t.runtimeLabel||null,productDir:r.productDir||t.productDir||null,serviceName:r.name,serviceStateDir:r.stateDir||t.serviceStateDir||null,prepareDir:r.testkit.prepareDir||t.prepareDir||null,leaseId:e?.leaseId||null,leaseDir:e?.leaseDir||null}}export function finalizeString(r,e){let t=resolveTemplateString(r,e);for(const[n,i]of e.urlMappings||[])n&&i&&n!==i&&(t=t.split(n).join(i));return t}export function finalizeSerializable(r,e){return typeof r=="string"?finalizeString(r,e):Array.isArray(r)?r.map(t=>finalizeSerializable(t,e)):r===null||typeof r!="object"?r:Object.fromEntries(Object.entries(r).map(([t,n])=>[t,finalizeSerializable(n,e)]))}export function resolveTemplateString(r,e){return typeof r!="string"?r:r.replace(/\{([a-zA-Z]+)(?::([^}]+))?\}/g,(t,n,i)=>{switch(n){case"runtime":case"runtimeId":return String(e.runtimeId);case"service":return e.serviceName;case"stateDir":return String(e.serviceStateDir);case"productDir":return e.productDir||"";case"bin":{const s=v.resolve(e.productDir||process.cwd(),e.serviceCwd||".");return w(E(s,i))}case"pathList":{const s=l(i||"").split(",").map(a=>a.trim()).filter(Boolean).map(a=>v.resolve(e.productDir||process.cwd(),a));return m(s)}case"prepareDir":return e.prepareDir||"";case"lease":return e.leaseId?String(e.leaseId):"";case"leaseDir":return e.leaseDir||"";case"port":{const s=i||e.serviceName,a=e.portMap?.get(s);if(!a)throw new Error(`Unknown port placeholder for service "${s}"`);return String(a)}case"baseUrl":{const s=i||e.serviceName,a=e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown baseUrl placeholder for service "${s}"`);return a}case"readyUrl":{const s=i||e.serviceName,a=e.readyUrlByService?.get(s);if(!a)throw new Error(`Unknown readyUrl placeholder for service "${s}"`);return a}case"publicHost":return e.publicHost||"127.0.0.1";case"publicBaseUrl":{const s=i||e.serviceName,a=e.publicBaseUrlByService?.get(s)||e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown publicBaseUrl placeholder for service "${s}"`);return a}case"privateBaseUrl":{const s=i||e.serviceName,a=e.privateBaseUrlByService?.get(s)||e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown privateBaseUrl placeholder for service "${s}"`);return a}case"serviceContextUrl":{const s=i||e.serviceName,a=e.serviceContextUrlByService?.get(s)||e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown serviceContextUrl placeholder for service "${s}"`);return a}case"dbUrl":case"dbHost":case"dbPort":case"dbName":case"dbUser":case"dbPassword":case"dbSsl":{const s=i||e.serviceName;return U(n,t,s,e)}default:throw new Error(`Unsupported template token "{${n}${i?`:${i}`:""}}"`)}})}function d(r,e){return Object.fromEntries(Object.entries(r||{}).map(([t,n])=>[t,typeof n=="string"?S(t,n,e):n]))}function S(r,e,t){try{return finalizeString(e,t)}catch(n){const i=n instanceof Error?n.message:l(n);throw new Error(`Service "${t.serviceName}" env ${r}: ${i}`,{cause:n})}}function c(r,e,t){return Object.fromEntries(Object.entries(r||{}).filter(([n,i])=>e&&!e.has(n)?!1:!(t&&typeof i=="string"&&findDatabasePlaceholders(i).length>0)))}export function findDatabasePlaceholders(r){return[...r.matchAll(/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::([^}]+))?\}/g)].map(e=>({placeholder:e[0],serviceName:e.at(1)??null}))}function U(r,e,t,n){const i=n.stateDirByService?.get(t),s=n.databaseBindingsByService?n.databaseBindingsByService.get(t):i?n.databaseBindingsReader?.(i):null;if(!s)throw new Error(`Cannot resolve ${e}: service "${t}" has no database binding in this context`);switch(r){case"dbUrl":return s.url;case"dbHost":return s.host;case"dbPort":return String(s.port);case"dbName":return s.database;case"dbUser":return s.user;case"dbPassword":return s.password;case"dbSsl":return D(s)}}function D(r){const e=r.sslMode,t=l(e||"").trim();return t&&!B(t)?t:h(r.sslMode)?"1":"0"}function h(r){const e=l(r||"").trim().toLowerCase();return!!(e&&!["0","false","no","off","disable","disabled"].includes(e))}function B(r){return["0","1","true","false","yes","no","on","off","disable","disabled","require","verify-ca","verify-full"].includes(l(r||"").trim().toLowerCase())}function l(r){const e=Reflect.apply(String,void 0,[r]);if(typeof e!="string")throw new Error("String conversion returned an invalid result");return e}
|
package/dist/steps/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { buildExecutionEnv, buildTaskExecutionEnv, buildTemplateContext, buildTemplateExecutionEnv, finalizeSerializable, finalizeString, resolveTemplateString, type BuildExecutionEnvironmentOptions, type DatabaseTemplateBindings, type DatabaseTemplateBindingsReader, type ExecutionEnvironmentConfig, type ExecutionEnvironmentLease, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues, type RuntimeUrlMapping, type TemplateResolutionContext, } from "./environment.js";
|
|
1
|
+
export { buildExecutionEnv, buildTaskExecutionEnv, buildTemplateContext, buildTemplateExecutionEnv, finalizeSerializable, finalizeString, findDatabasePlaceholders, resolveExecutionEnvValues, resolveTemplateString, type BuildExecutionEnvironmentOptions, type DatabasePlaceholder, type DatabaseTemplateBindings, type DatabaseTemplateBindingsReader, type ExecutionEnvironmentConfig, type ExecutionEnvironmentLease, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues, type RuntimeUrlMapping, type TemplateResolutionContext, } from "./environment.js";
|
|
2
2
|
export { collectConfiguredInputs, configuredCommandInvocation, parseModuleSpecifier, resolveConfiguredCwd, resolveConfiguredPath, type ConfiguredCommandInvocation, type ConfiguredStepReporter, type ConfiguredStepServiceConfig, } from "./run.js";
|
package/dist/steps/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{buildExecutionEnv,buildTaskExecutionEnv,buildTemplateContext,buildTemplateExecutionEnv,finalizeSerializable,finalizeString,resolveTemplateString}from"./environment.js";export{collectConfiguredInputs,configuredCommandInvocation,parseModuleSpecifier,resolveConfiguredCwd,resolveConfiguredPath}from"./run.js";
|
|
1
|
+
export{buildExecutionEnv,buildTaskExecutionEnv,buildTemplateContext,buildTemplateExecutionEnv,finalizeSerializable,finalizeString,findDatabasePlaceholders,resolveExecutionEnvValues,resolveTemplateString}from"./environment.js";export{collectConfiguredInputs,configuredCommandInvocation,parseModuleSpecifier,resolveConfiguredCwd,resolveConfiguredPath}from"./run.js";
|
package/package.json
CHANGED
package/testkit-config-schema.md
CHANGED
|
@@ -333,11 +333,63 @@ Template tokens are supported in strings:
|
|
|
333
333
|
- `{port}` / `{port:serviceName}`
|
|
334
334
|
- `{baseUrl}` / `{baseUrl:serviceName}`
|
|
335
335
|
- `{readyUrl}` / `{readyUrl:serviceName}`
|
|
336
|
+
- `{dbUrl}`, `{dbHost}`, `{dbPort}`, `{dbName}`, `{dbUser}`, `{dbPassword}`,
|
|
337
|
+
`{dbSsl}`, each with an optional `:serviceName` (env values only, see
|
|
338
|
+
[`env`](#env))
|
|
336
339
|
|
|
337
340
|
### `dependsOn`
|
|
338
341
|
|
|
339
342
|
Runtime dependencies for service startup order.
|
|
340
343
|
|
|
344
|
+
### `env`
|
|
345
|
+
|
|
346
|
+
Declared environment values for the service process and every task that targets
|
|
347
|
+
the service. Plain services take a string record. `app.node(...)` and
|
|
348
|
+
`app.next(...)` take `{ values, databases }` and place the result under
|
|
349
|
+
`local.env`; `local.env` overrides `env` when both set a name.
|
|
350
|
+
|
|
351
|
+
Values support the template tokens listed under [`local`](#local), including
|
|
352
|
+
the database tokens:
|
|
353
|
+
|
|
354
|
+
| Token | Value |
|
|
355
|
+
| -------------- | --------------------------------------- |
|
|
356
|
+
| `{dbUrl}` | connection URL |
|
|
357
|
+
| `{dbHost}` | host |
|
|
358
|
+
| `{dbPort}` | host port |
|
|
359
|
+
| `{dbName}` | database name |
|
|
360
|
+
| `{dbUser}` | user |
|
|
361
|
+
| `{dbPassword}` | password |
|
|
362
|
+
| `{dbSsl}` | `1` when the binding uses SSL, else `0` |
|
|
363
|
+
|
|
364
|
+
The bare form reads the service's own database, which must be declared under
|
|
365
|
+
`database`. `{dbUrl:api}` reads the database of service `api`, which must be
|
|
366
|
+
listed in `dependsOn` so it is provisioned first. Testkit resolves these tokens
|
|
367
|
+
after provisioning, when it runs `runtime.prepare` steps, starts the service
|
|
368
|
+
process, and builds each task environment, from the database provisioned for
|
|
369
|
+
that run. A repeated run never sees a previous run's connection details.
|
|
370
|
+
|
|
371
|
+
Database tokens resolve only in `env` and `local.env` values. Testkit rejects
|
|
372
|
+
them at config load in `local.start`, `local.cwd`, `runtime.prepare`, and
|
|
373
|
+
`database.template` strings, because it resolves those strings while laying out
|
|
374
|
+
the runtime, before any database exists. Commands read `DATABASE_URL` or a
|
|
375
|
+
declared env entry at run time instead.
|
|
376
|
+
|
|
377
|
+
Source-schema discovery fails when the env entry it reads as the source URL
|
|
378
|
+
points at the service's own database. A source schema has to come from a
|
|
379
|
+
database other than the one it verifies.
|
|
380
|
+
|
|
381
|
+
`env.databases` expands a binding into prefixed entries:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
env: {
|
|
385
|
+
databases: { api: { prefix: "API", service: "api" } },
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
produces `API_DATABASE_HOST`, `API_DATABASE_PORT`, `API_DATABASE_NAME`,
|
|
390
|
+
`API_DATABASE_USER`, `API_DATABASE_PASSWORD`, and `API_DATABASE_SSL`, each set
|
|
391
|
+
to the matching `{db*:api}` token. `env.values` entries override expanded names.
|
|
392
|
+
|
|
341
393
|
### `envFile` / `envFiles`
|
|
342
394
|
|
|
343
395
|
Optional extra env files loaded into the declared service/task environment for
|