@augment-vir/node 31.0.0 → 31.1.0
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/dist/augments/fs/dir-contents.js +1 -1
- package/dist/augments/terminal/relevant-args.d.ts +1 -7
- package/dist/augments/terminal/relevant-args.js +1 -7
- package/dist/augments/terminal/run-cli-script.js +1 -5
- package/dist/augments/terminal/shell.js +1 -4
- package/dist/docker/containers/docker-command-inputs.js +1 -1
- package/dist/docker/containers/run-container.mock.js +1 -4
- package/dist/docker/docker-image.js +1 -6
- package/dist/prisma/model-data.js +2 -5
- package/dist/prisma/prisma-migrations.js +2 -7
- package/dist/prisma/run-prisma-command.js +3 -15
- package/dist/scripts/fix-ts-bin.script.d.ts +1 -0
- package/dist/scripts/fix-ts-bin.script.js +48 -0
- package/package.json +4 -4
- package/src/augments/docker.ts +118 -0
- package/src/augments/fs/dir-contents.ts +116 -0
- package/src/augments/fs/download.ts +31 -0
- package/src/augments/fs/json.ts +87 -0
- package/src/augments/fs/read-dir.ts +60 -0
- package/src/augments/fs/read-file.ts +18 -0
- package/src/augments/fs/symlink.ts +37 -0
- package/src/augments/fs/write.ts +18 -0
- package/src/augments/npm/query-workspace.ts +47 -0
- package/src/augments/npm/read-package-json.ts +28 -0
- package/src/augments/os/operating-system.ts +55 -0
- package/src/augments/path/ancestor.ts +78 -0
- package/src/augments/path/os-path.ts +45 -0
- package/src/augments/path/root.ts +14 -0
- package/src/augments/prisma.ts +174 -0
- package/src/augments/terminal/question.ts +142 -0
- package/src/augments/terminal/relevant-args.ts +81 -0
- package/src/augments/terminal/run-cli-script.ts +60 -0
- package/src/augments/terminal/shell.ts +312 -0
- package/src/docker/containers/container-info.ts +83 -0
- package/src/docker/containers/container-status.ts +110 -0
- package/src/docker/containers/copy-to-container.ts +34 -0
- package/src/docker/containers/docker-command-inputs.ts +119 -0
- package/src/docker/containers/kill-container.ts +25 -0
- package/src/docker/containers/run-command.ts +51 -0
- package/src/docker/containers/run-container.mock.ts +17 -0
- package/src/docker/containers/run-container.ts +92 -0
- package/src/docker/containers/try-or-kill-container.ts +18 -0
- package/src/docker/docker-image.ts +56 -0
- package/src/docker/docker-startup.ts +49 -0
- package/src/docker/run-docker-test.mock.ts +26 -0
- package/src/file-paths.mock.ts +29 -0
- package/src/index.ts +19 -0
- package/src/prisma/disable-ci-env.mock.ts +88 -0
- package/src/prisma/model-data.ts +213 -0
- package/src/prisma/prisma-client.ts +43 -0
- package/src/prisma/prisma-database.mock.ts +31 -0
- package/src/prisma/prisma-database.ts +35 -0
- package/src/prisma/prisma-errors.ts +45 -0
- package/src/prisma/prisma-migrations.ts +149 -0
- package/src/prisma/run-prisma-command.ts +59 -0
- package/src/scripts/fix-ts-bin.script.ts +60 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// cspell:disable
|
|
2
|
+
|
|
3
|
+
import {arrayToObject, type MaybePromise} from '@augment-vir/common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* These are all the env flags that Prisma reads for determining if it's being executed within a CI
|
|
7
|
+
* environment. This list was retrieved from
|
|
8
|
+
* https://github.com/prisma/prisma/blob/075d31287c90b757fd9bd8d9b36032e6349fa671/packages/internals/src/utils/isCi.ts.
|
|
9
|
+
*/
|
|
10
|
+
const prismaCiFlags = [
|
|
11
|
+
'CI',
|
|
12
|
+
'CONTINUOUS_INTEGRATION',
|
|
13
|
+
'BUILD_NUMBER',
|
|
14
|
+
'RUN_ID',
|
|
15
|
+
'AGOLA_GIT_REF',
|
|
16
|
+
'AC_APPCIRCLE',
|
|
17
|
+
'APPVEYOR',
|
|
18
|
+
'CODEBUILD',
|
|
19
|
+
'TF_BUILD',
|
|
20
|
+
'bamboo_planKey',
|
|
21
|
+
'BITBUCKET_COMMIT',
|
|
22
|
+
'BITRISE_IO',
|
|
23
|
+
'BUDDY_WORKSPACE_ID',
|
|
24
|
+
'BUILDKITE',
|
|
25
|
+
'CIRCLECI',
|
|
26
|
+
'CIRRUS_CI',
|
|
27
|
+
'CF_BUILD_ID',
|
|
28
|
+
'CM_BUILD_ID',
|
|
29
|
+
'CI_NAME',
|
|
30
|
+
'DRONE',
|
|
31
|
+
'DSARI',
|
|
32
|
+
'EARTHLY_CI',
|
|
33
|
+
'EAS_BUILD',
|
|
34
|
+
'GERRIT_PROJECT',
|
|
35
|
+
'GITEA_ACTIONS',
|
|
36
|
+
'GITHUB_ACTIONS',
|
|
37
|
+
'GITLAB_CI',
|
|
38
|
+
'GOCD',
|
|
39
|
+
'BUILDER_OUTPUT',
|
|
40
|
+
'HARNESS_BUILD_ID',
|
|
41
|
+
'JENKINS_URL',
|
|
42
|
+
'BUILD_ID',
|
|
43
|
+
'LAYERCI',
|
|
44
|
+
'MAGNUM',
|
|
45
|
+
'NETLIFY',
|
|
46
|
+
'NEVERCODE',
|
|
47
|
+
'PROW_JOB_ID',
|
|
48
|
+
'RELEASE_BUILD_ID',
|
|
49
|
+
'RENDER',
|
|
50
|
+
'SAILCI',
|
|
51
|
+
'HUDSON',
|
|
52
|
+
'JENKINS_URL',
|
|
53
|
+
'BUILD_ID',
|
|
54
|
+
'SCREWDRIVER',
|
|
55
|
+
'SEMAPHORE',
|
|
56
|
+
'SOURCEHUT',
|
|
57
|
+
'STRIDER',
|
|
58
|
+
'TASK_ID',
|
|
59
|
+
'RUN_ID',
|
|
60
|
+
'TEAMCITY_VERSION',
|
|
61
|
+
'TRAVIS',
|
|
62
|
+
'VELA',
|
|
63
|
+
'NOW_BUILDER',
|
|
64
|
+
'APPCENTER_BUILD_ID',
|
|
65
|
+
'CI_XCODE_PROJECT',
|
|
66
|
+
'XCS',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export function testWithNonCiEnv(callback: () => MaybePromise<void>) {
|
|
70
|
+
return async () => {
|
|
71
|
+
const usedKeys = prismaCiFlags.filter((ciFlag) => ciFlag in process.env);
|
|
72
|
+
|
|
73
|
+
/** For already non-CI environments. */
|
|
74
|
+
/* node:coverage ignore next 6 */
|
|
75
|
+
const originalEnvValues = arrayToObject(usedKeys, (key) => {
|
|
76
|
+
return {
|
|
77
|
+
key,
|
|
78
|
+
value: process.env[key],
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
usedKeys.forEach((key) => delete process.env[key]);
|
|
83
|
+
|
|
84
|
+
await callback();
|
|
85
|
+
|
|
86
|
+
usedKeys.forEach((key) => (process.env[key] = originalEnvValues[key]));
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {assert, check} from '@augment-vir/assert';
|
|
2
|
+
import {
|
|
3
|
+
AnyObject,
|
|
4
|
+
arrayToObject,
|
|
5
|
+
awaitedForEach,
|
|
6
|
+
BasePrismaClient,
|
|
7
|
+
ensureErrorAndPrependMessage,
|
|
8
|
+
filterMap,
|
|
9
|
+
getObjectTypedEntries,
|
|
10
|
+
getObjectTypedValues,
|
|
11
|
+
mergeDefinedProperties,
|
|
12
|
+
omitObjectKeys,
|
|
13
|
+
PrismaAllModelsCreate,
|
|
14
|
+
prismaModelCreateExclude,
|
|
15
|
+
prismaModelCreateOmitId,
|
|
16
|
+
type PartialWithUndefined,
|
|
17
|
+
type PrismaAllBasicModels,
|
|
18
|
+
type PrismaModelName,
|
|
19
|
+
} from '@augment-vir/common';
|
|
20
|
+
import type {IsAny} from 'type-fest';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Params for {@link addData}. This is similar to {@link PrismaAllModelsCreate} but allows an array of
|
|
24
|
+
* {@link PrismaAllModelsCreate} for sequential data creation.
|
|
25
|
+
*
|
|
26
|
+
* @category Prisma : Node
|
|
27
|
+
* @category Package : @augment-vir/node
|
|
28
|
+
* @example
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* import {PrismaAddModelData} from '@augment-vir/common';
|
|
32
|
+
* import type {PrismaClient} from '@prisma/client';
|
|
33
|
+
*
|
|
34
|
+
* const mockData: PrismaAddModelData<PrismaClient> = [
|
|
35
|
+
* {
|
|
36
|
+
* user: {
|
|
37
|
+
* mockUser1: {
|
|
38
|
+
* first_name: 'one',
|
|
39
|
+
* id: 123,
|
|
40
|
+
* // etc.
|
|
41
|
+
* },
|
|
42
|
+
* mockUser2: {
|
|
43
|
+
* first_name: 'two',
|
|
44
|
+
* id: 124,
|
|
45
|
+
* authRole: 'user',
|
|
46
|
+
* // etc.
|
|
47
|
+
* },
|
|
48
|
+
* },
|
|
49
|
+
* },
|
|
50
|
+
* {
|
|
51
|
+
* region: [
|
|
52
|
+
* {
|
|
53
|
+
* id: 1,
|
|
54
|
+
* name: 'North America',
|
|
55
|
+
* // etc.
|
|
56
|
+
* },
|
|
57
|
+
* {
|
|
58
|
+
* id: 2,
|
|
59
|
+
* name: 'Europe',
|
|
60
|
+
* // etc.
|
|
61
|
+
* },
|
|
62
|
+
* ],
|
|
63
|
+
* },
|
|
64
|
+
* ];
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
68
|
+
*/
|
|
69
|
+
export type PrismaAddDataData<PrismaClient extends BasePrismaClient> =
|
|
70
|
+
| Readonly<PrismaAllModelsCreate<PrismaClient>>
|
|
71
|
+
| ReadonlyArray<Readonly<PrismaAllModelsCreate<PrismaClient>>>;
|
|
72
|
+
|
|
73
|
+
export async function addData<const PrismaClient extends BasePrismaClient>(
|
|
74
|
+
prismaClient: Readonly<PrismaClient>,
|
|
75
|
+
data: IsAny<PrismaClient> extends true ? any : PrismaAddDataData<PrismaClient>,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const dataArray: Record<string, AnyObject>[] = (check.isArray(data) ? data : [data]) as Record<
|
|
78
|
+
string,
|
|
79
|
+
AnyObject
|
|
80
|
+
>[];
|
|
81
|
+
|
|
82
|
+
await awaitedForEach(dataArray, async (dataEntry) => {
|
|
83
|
+
await addModelDataObject(prismaClient, dataEntry);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function addModelDataObject(
|
|
88
|
+
prismaClient: Readonly<BasePrismaClient>,
|
|
89
|
+
data: Record<string, AnyObject>,
|
|
90
|
+
) {
|
|
91
|
+
/** Add the mock data to the mock prisma client. */
|
|
92
|
+
await awaitedForEach(getObjectTypedEntries(data), async ([modelName, mockData]) => {
|
|
93
|
+
/**
|
|
94
|
+
* This type is dumbed down to just `AnyObject[]` because the union of all possible
|
|
95
|
+
* model data is just way too big (and not helpful as the inputs to this function are
|
|
96
|
+
* already type guarded).
|
|
97
|
+
*/
|
|
98
|
+
const mockModelInstances: AnyObject[] = Array.isArray(mockData)
|
|
99
|
+
? mockData
|
|
100
|
+
: getObjectTypedValues(mockData);
|
|
101
|
+
|
|
102
|
+
const modelApi: AnyObject | undefined = prismaClient[modelName];
|
|
103
|
+
|
|
104
|
+
assert.isDefined(modelApi, `No PrismaClient API found for model '${modelName}'`);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const allData = filterMap(
|
|
108
|
+
mockModelInstances,
|
|
109
|
+
(entry) => {
|
|
110
|
+
return entry;
|
|
111
|
+
},
|
|
112
|
+
(mapped, modelEntry) => !modelEntry[prismaModelCreateExclude],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await awaitedForEach(allData, async (modelEntry) => {
|
|
116
|
+
if (modelEntry[prismaModelCreateOmitId]) {
|
|
117
|
+
modelEntry = omitObjectKeys<AnyObject, PropertyKey>(modelEntry, ['id']);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await modelApi.create({
|
|
121
|
+
data: modelEntry,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw ensureErrorAndPrependMessage(
|
|
126
|
+
error,
|
|
127
|
+
`Failed to create many '${modelName}' entries.\n\n${JSON.stringify(mockModelInstances, null, 4)}\n\n`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const prismockKeys = ['getData', 'setData'];
|
|
134
|
+
|
|
135
|
+
export function getAllPrismaModelNames<const PrismaClient extends BasePrismaClient>(
|
|
136
|
+
prismaClient: PrismaClient,
|
|
137
|
+
): PrismaModelName<PrismaClient>[] {
|
|
138
|
+
return Object.keys(prismaClient)
|
|
139
|
+
.filter(
|
|
140
|
+
(key) => !key.startsWith('$') && !key.startsWith('_') && !prismockKeys.includes(key),
|
|
141
|
+
)
|
|
142
|
+
.sort() as PrismaModelName<PrismaClient>[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Options for `prisma.client.dumpData`.
|
|
147
|
+
*
|
|
148
|
+
* @category Prisma : Node
|
|
149
|
+
* @category Package : @augment-vir/node
|
|
150
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
151
|
+
*/
|
|
152
|
+
export type PrismaDataDumpOptions = {
|
|
153
|
+
/**
|
|
154
|
+
* The max number of entries to load per model. Set to `0` to remove this limit altogether
|
|
155
|
+
* (which could be _very_ expensive for your database).
|
|
156
|
+
*
|
|
157
|
+
* @default 100
|
|
158
|
+
*/
|
|
159
|
+
limit: number;
|
|
160
|
+
/**
|
|
161
|
+
* Strings to omit from the dumped data. For testability, omitting date or UUID id fields is a
|
|
162
|
+
* common practice.
|
|
163
|
+
*/
|
|
164
|
+
omitFields: string[];
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const defaultPrismaDumpDataOptions: PrismaDataDumpOptions = {
|
|
168
|
+
limit: 100,
|
|
169
|
+
omitFields: [],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export async function dumpData<const PrismaClient extends BasePrismaClient>(
|
|
173
|
+
prismaClient: PrismaClient,
|
|
174
|
+
options: Readonly<PartialWithUndefined<PrismaDataDumpOptions>> = {},
|
|
175
|
+
): Promise<PrismaAllBasicModels<PrismaClient>> {
|
|
176
|
+
const modelNames = getAllPrismaModelNames(prismaClient);
|
|
177
|
+
const finalOptions = mergeDefinedProperties(defaultPrismaDumpDataOptions, options);
|
|
178
|
+
|
|
179
|
+
const data: Partial<Record<PrismaModelName<PrismaClient>, AnyObject[]>> = await arrayToObject(
|
|
180
|
+
modelNames,
|
|
181
|
+
async (modelName) => {
|
|
182
|
+
try {
|
|
183
|
+
const entries: AnyObject[] = await prismaClient[modelName].findMany(
|
|
184
|
+
finalOptions.limit > 0
|
|
185
|
+
? {
|
|
186
|
+
take: finalOptions.limit,
|
|
187
|
+
}
|
|
188
|
+
: {},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!entries.length) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const filteredEntries = finalOptions.omitFields.length
|
|
196
|
+
? entries.map((entry) => omitObjectKeys(entry, finalOptions.omitFields))
|
|
197
|
+
: entries;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
key: modelName,
|
|
201
|
+
value: filteredEntries,
|
|
202
|
+
};
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw ensureErrorAndPrependMessage(
|
|
205
|
+
error,
|
|
206
|
+
`Failed to read data for model '${modelName}'`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return data as PrismaAllBasicModels<PrismaClient>;
|
|
213
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {collapseWhiteSpace} from '@augment-vir/common';
|
|
2
|
+
import {existsSync} from 'node:fs';
|
|
3
|
+
import {readFile} from 'node:fs/promises';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {runPrismaCommand} from './run-prisma-command.js';
|
|
6
|
+
|
|
7
|
+
export async function generatePrismaClient(
|
|
8
|
+
schemaFilePath: string,
|
|
9
|
+
env: Record<string, string> = {},
|
|
10
|
+
) {
|
|
11
|
+
await runPrismaCommand({command: 'generate'}, schemaFilePath, env);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function areSchemasEqual(
|
|
15
|
+
originalSchema: string,
|
|
16
|
+
generatedClientSchema: string,
|
|
17
|
+
): Promise<boolean> {
|
|
18
|
+
if (!existsSync(originalSchema)) {
|
|
19
|
+
throw new Error(`Schema file does not exist: '${originalSchema}'`);
|
|
20
|
+
} else if (!existsSync(generatedClientSchema)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const originalSchemaContents: string = String(await readFile(originalSchema));
|
|
25
|
+
const generatedClientSchemaContents: string = String(await readFile(generatedClientSchema));
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
collapseWhiteSpace(originalSchemaContents, {keepNewLines: true}) ===
|
|
29
|
+
collapseWhiteSpace(generatedClientSchemaContents, {keepNewLines: true})
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function isGeneratedPrismaClientCurrent({
|
|
34
|
+
jsClientOutputDir,
|
|
35
|
+
schemaFilePath,
|
|
36
|
+
}: {
|
|
37
|
+
schemaFilePath: string;
|
|
38
|
+
jsClientOutputDir: string;
|
|
39
|
+
}) {
|
|
40
|
+
const clientSchemaFilePath = join(jsClientOutputDir, 'schema.prisma');
|
|
41
|
+
|
|
42
|
+
return areSchemasEqual(schemaFilePath, clientSchemaFilePath);
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {awaitedForEach, callWithRetries, wait} from '@augment-vir/common';
|
|
2
|
+
import {interpolationSafeWindowsPath} from '../augments/path/os-path.js';
|
|
3
|
+
import {runShellCommand} from '../augments/terminal/shell.js';
|
|
4
|
+
import {
|
|
5
|
+
generatedPrismaClientDirPath,
|
|
6
|
+
notCommittedDirPath,
|
|
7
|
+
testPrismaMigrationsDirPath,
|
|
8
|
+
} from '../file-paths.mock.js';
|
|
9
|
+
|
|
10
|
+
const pathsToDelete = [
|
|
11
|
+
generatedPrismaClientDirPath,
|
|
12
|
+
notCommittedDirPath,
|
|
13
|
+
testPrismaMigrationsDirPath,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export async function clearTestDatabaseOutputs() {
|
|
17
|
+
await callWithRetries(10, async () => {
|
|
18
|
+
await wait({seconds: 1});
|
|
19
|
+
await awaitedForEach(pathsToDelete, async (pathToDelete) => {
|
|
20
|
+
/**
|
|
21
|
+
* This way of deleting files is required for Windows tests running on GitHub Actions.
|
|
22
|
+
* Otherwise, we get the following error:
|
|
23
|
+
*
|
|
24
|
+
* EPERM: operation not permitted, unlink 'D:\a\augment-vir\augment-vir\packages\node\node_modules\.prisma\query_engine-windows.dll.node'
|
|
25
|
+
*/
|
|
26
|
+
await runShellCommand(`rm -rf ${interpolationSafeWindowsPath(pathToDelete)}`, {
|
|
27
|
+
rejectOnError: true,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {runPrismaCommand} from './run-prisma-command.js';
|
|
2
|
+
|
|
3
|
+
export async function getPrismaDiff(
|
|
4
|
+
schemaFilePath: string,
|
|
5
|
+
env: Record<string, string> = {},
|
|
6
|
+
): Promise<string> {
|
|
7
|
+
const command = [
|
|
8
|
+
'migrate',
|
|
9
|
+
'diff',
|
|
10
|
+
`--from-schema-datamodel='${schemaFilePath}'`,
|
|
11
|
+
`--to-schema-datasource='${schemaFilePath}'`,
|
|
12
|
+
].join(' ');
|
|
13
|
+
|
|
14
|
+
const results = await runPrismaCommand({command}, undefined, env);
|
|
15
|
+
|
|
16
|
+
if (results.stdout.trim() === 'No difference detected.') {
|
|
17
|
+
return '';
|
|
18
|
+
} else {
|
|
19
|
+
return results.stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function doesPrismaDiffExist(
|
|
24
|
+
schemaFilePath: string,
|
|
25
|
+
env: Record<string, string> = {},
|
|
26
|
+
): Promise<boolean> {
|
|
27
|
+
return !!(await getPrismaDiff(schemaFilePath, env));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function resetDevPrismaDatabase(
|
|
31
|
+
schemaFilePath: string,
|
|
32
|
+
env: Record<string, string> = {},
|
|
33
|
+
) {
|
|
34
|
+
await runPrismaCommand({command: 'migrate reset --force'}, schemaFilePath, env);
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An error thrown by the Prisma API that indicates that something is wrong with the given Prisma
|
|
3
|
+
* schema. See the error message for more details.
|
|
4
|
+
*
|
|
5
|
+
* @category Prisma : Node : Util
|
|
6
|
+
* @category Package : @augment-vir/node
|
|
7
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
8
|
+
*/
|
|
9
|
+
export class PrismaSchemaError extends Error {
|
|
10
|
+
public override readonly name = 'PrismaSchemaError';
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An error thrown by the Prisma API that indicates that applying migrations in dev failed such that
|
|
18
|
+
* a new migration is needed. You can do that by prompting user for a new migration name and passing
|
|
19
|
+
* it to `prisma.migration.create`.
|
|
20
|
+
*
|
|
21
|
+
* @category Prisma : Node : Util
|
|
22
|
+
* @category Package : @augment-vir/node
|
|
23
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
24
|
+
*/
|
|
25
|
+
export class PrismaMigrationNeededError extends Error {
|
|
26
|
+
public override readonly name = 'PrismaMigrationNeededError';
|
|
27
|
+
constructor(schemaFilePath: string) {
|
|
28
|
+
super(`A new Prisma migration is needed for '${schemaFilePath}'`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* An error thrown by the Prisma API that indicates that applying migrations in dev failed such that
|
|
34
|
+
* the entire database needs to be reset. You can do that by calling `prisma.database.resetDev`.
|
|
35
|
+
*
|
|
36
|
+
* @category Prisma : Node : Util
|
|
37
|
+
* @category Package : @augment-vir/node
|
|
38
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
39
|
+
*/
|
|
40
|
+
export class PrismaResetNeededError extends Error {
|
|
41
|
+
public override readonly name = 'PrismaResetNeededError';
|
|
42
|
+
constructor(schemaFilePath: string) {
|
|
43
|
+
super(`A database reset is needed for '${schemaFilePath}'`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {check} from '@augment-vir/assert';
|
|
2
|
+
import {log, safeMatch, toEnsuredNumber} from '@augment-vir/common';
|
|
3
|
+
import terminate from 'terminate';
|
|
4
|
+
import {runShellCommand} from '../augments/terminal/shell.js';
|
|
5
|
+
import {PrismaMigrationNeededError, PrismaResetNeededError} from './prisma-errors.js';
|
|
6
|
+
import {runPrismaCommand, verifyOutput} from './run-prisma-command.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Output of `prisma.migration.status`.
|
|
10
|
+
*
|
|
11
|
+
* @category Prisma : Node : Util
|
|
12
|
+
* @category Package : @augment-vir/node
|
|
13
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
14
|
+
*/
|
|
15
|
+
export type PrismaMigrationStatus = {
|
|
16
|
+
totalMigrations: number;
|
|
17
|
+
unappliedMigrations: string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function applyPrismaMigrationsToProd(
|
|
21
|
+
schemaFilePath: string,
|
|
22
|
+
env: Record<string, string> = {},
|
|
23
|
+
) {
|
|
24
|
+
await runPrismaCommand({command: 'migrate deploy'}, schemaFilePath, env);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enum DbChangeRequired {
|
|
28
|
+
MigrationNeeded = 'migration-needed',
|
|
29
|
+
ResetNeeded = 'reset-needed',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function applyPrismaMigrationsToDev(
|
|
33
|
+
schemaFilePath: string,
|
|
34
|
+
env: Record<string, string> = {},
|
|
35
|
+
) {
|
|
36
|
+
const command = ['prisma', 'migrate', 'dev', `--schema='${schemaFilePath}'`].join(' ');
|
|
37
|
+
|
|
38
|
+
log.faint(`> ${command}`);
|
|
39
|
+
|
|
40
|
+
let dbRequirement = undefined as DbChangeRequired | undefined;
|
|
41
|
+
|
|
42
|
+
const result = await runShellCommand(command, {
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
...env,
|
|
46
|
+
},
|
|
47
|
+
stdoutCallback(stdout, childProcess) {
|
|
48
|
+
if (stdout.includes('Enter a name for the new migration')) {
|
|
49
|
+
if (childProcess.pid) {
|
|
50
|
+
terminate(childProcess.pid);
|
|
51
|
+
}
|
|
52
|
+
dbRequirement = DbChangeRequired.MigrationNeeded;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
stderrCallback(stderr, childProcess) {
|
|
56
|
+
if (
|
|
57
|
+
stderr.includes(
|
|
58
|
+
'Prisma Migrate has detected that the environment is non-interactive, which is not supported',
|
|
59
|
+
)
|
|
60
|
+
) {
|
|
61
|
+
if (childProcess.pid) {
|
|
62
|
+
terminate(childProcess.pid);
|
|
63
|
+
}
|
|
64
|
+
dbRequirement = DbChangeRequired.ResetNeeded;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (dbRequirement === DbChangeRequired.MigrationNeeded) {
|
|
70
|
+
throw new PrismaMigrationNeededError(schemaFilePath);
|
|
71
|
+
} else if (dbRequirement === DbChangeRequired.ResetNeeded) {
|
|
72
|
+
throw new PrismaResetNeededError(schemaFilePath);
|
|
73
|
+
}
|
|
74
|
+
verifyOutput(schemaFilePath, result, false);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function getMigrationStatus(
|
|
78
|
+
schemaFilePath: string,
|
|
79
|
+
env: Record<string, string> = {},
|
|
80
|
+
): Promise<PrismaMigrationStatus> {
|
|
81
|
+
const output = await runPrismaCommand(
|
|
82
|
+
{
|
|
83
|
+
command: 'migrate status',
|
|
84
|
+
ignoreExitCode: true,
|
|
85
|
+
},
|
|
86
|
+
schemaFilePath,
|
|
87
|
+
env,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const listedMigrations: PrismaMigrationStatus = {
|
|
91
|
+
totalMigrations: 0,
|
|
92
|
+
unappliedMigrations: [],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
let foundNotAppliedMigrations = false;
|
|
96
|
+
|
|
97
|
+
output.stdout.split('\n').some((rawLine) => {
|
|
98
|
+
const line = rawLine.trim();
|
|
99
|
+
if (foundNotAppliedMigrations) {
|
|
100
|
+
if (line) {
|
|
101
|
+
listedMigrations.unappliedMigrations.push(line);
|
|
102
|
+
} else {
|
|
103
|
+
/** We're done parsing. */
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
} else if (line.endsWith('not yet been applied:')) {
|
|
107
|
+
foundNotAppliedMigrations = true;
|
|
108
|
+
} else {
|
|
109
|
+
const [, countMatch] = safeMatch(line, /^([\d,]+) migrations? found in/);
|
|
110
|
+
|
|
111
|
+
if (countMatch) {
|
|
112
|
+
listedMigrations.totalMigrations = toEnsuredNumber(countMatch);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Still need to keep parsing. */
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return listedMigrations;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function createPrismaMigration(
|
|
124
|
+
{
|
|
125
|
+
migrationName,
|
|
126
|
+
createOnly = false,
|
|
127
|
+
}: {
|
|
128
|
+
migrationName: string;
|
|
129
|
+
/**
|
|
130
|
+
* Set this to `true` to create a new migration without applying it to the database.
|
|
131
|
+
*
|
|
132
|
+
* @default false
|
|
133
|
+
*/
|
|
134
|
+
createOnly?: boolean | undefined;
|
|
135
|
+
},
|
|
136
|
+
schemaFilePath: string,
|
|
137
|
+
env: Record<string, string> = {},
|
|
138
|
+
) {
|
|
139
|
+
const command = [
|
|
140
|
+
'migrate',
|
|
141
|
+
'dev',
|
|
142
|
+
`--name='${migrationName}'`,
|
|
143
|
+
createOnly ? '--create-only' : '',
|
|
144
|
+
]
|
|
145
|
+
.filter(check.isTruthy)
|
|
146
|
+
.join(' ');
|
|
147
|
+
|
|
148
|
+
await runPrismaCommand({command}, schemaFilePath, env);
|
|
149
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {log} from '@augment-vir/common';
|
|
2
|
+
import {interpolationSafeWindowsPath} from '../augments/path/os-path.js';
|
|
3
|
+
import {runShellCommand, type ShellOutput} from '../augments/terminal/shell.js';
|
|
4
|
+
import {PrismaSchemaError} from './prisma-errors.js';
|
|
5
|
+
|
|
6
|
+
const prismaCommandsThatSupportNoHints = ['generate'];
|
|
7
|
+
|
|
8
|
+
export async function runPrismaCommand(
|
|
9
|
+
{
|
|
10
|
+
command,
|
|
11
|
+
ignoreExitCode = false,
|
|
12
|
+
}: {
|
|
13
|
+
command: string;
|
|
14
|
+
ignoreExitCode?: boolean | undefined;
|
|
15
|
+
},
|
|
16
|
+
/** Set to `undefined` to omit the `--schema` flag. */
|
|
17
|
+
schemaFilePath: string | undefined,
|
|
18
|
+
env: Record<string, string> | undefined = {},
|
|
19
|
+
) {
|
|
20
|
+
const schemaFileArgs = schemaFilePath ? ['--schema', schemaFilePath] : [];
|
|
21
|
+
|
|
22
|
+
/** Disable Prisma's in-CLI ads. */
|
|
23
|
+
const noHintsArg = prismaCommandsThatSupportNoHints.some((commandName) =>
|
|
24
|
+
command.startsWith(commandName),
|
|
25
|
+
)
|
|
26
|
+
? '--no-hints'
|
|
27
|
+
: '';
|
|
28
|
+
|
|
29
|
+
const fullCommand = ['prisma', command, ...schemaFileArgs, noHintsArg].join(' ');
|
|
30
|
+
|
|
31
|
+
log.faint(`> ${fullCommand}`);
|
|
32
|
+
|
|
33
|
+
const result = await runShellCommand(interpolationSafeWindowsPath(fullCommand), {
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
...env,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return verifyOutput(schemaFilePath || '', result, ignoreExitCode);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function verifyOutput(
|
|
44
|
+
schemaFilePath: string,
|
|
45
|
+
shellOutput: Readonly<ShellOutput>,
|
|
46
|
+
ignoreExitCode: boolean,
|
|
47
|
+
) {
|
|
48
|
+
if (shellOutput.stderr.includes('Validation Error Count')) {
|
|
49
|
+
throw new PrismaSchemaError(
|
|
50
|
+
`Invalid schema file at '${schemaFilePath}':\n\n${shellOutput.stderr}`,
|
|
51
|
+
);
|
|
52
|
+
} else if (shellOutput.stderr.includes('does not exist at')) {
|
|
53
|
+
throw new PrismaSchemaError(`Database does not exist: ${shellOutput.stderr}`);
|
|
54
|
+
} else if (shellOutput.exitCode === 0 || ignoreExitCode) {
|
|
55
|
+
return shellOutput;
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(shellOutput.stdout + shellOutput.stderr);
|
|
58
|
+
}
|
|
59
|
+
}
|