sanity 3.26.2-canary.52 → 3.26.2-canary.69
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/lib/_chunks/{_internal-2CJ5wSKF.js → _internal-6Pl2wJGj.js} +422 -201
- package/lib/_chunks/_internal-6Pl2wJGj.js.map +1 -0
- package/lib/_chunks/{_internal-79flWvL8.js → _internal-yQwMw2ht.js} +419 -203
- package/lib/_chunks/_internal-yQwMw2ht.js.map +1 -0
- package/lib/_chunks/{_internalBrowser-QAFz3SKp.js → _internalBrowser-HWvRXvlU.js} +22 -22
- package/lib/_chunks/_internalBrowser-HWvRXvlU.js.map +1 -0
- package/lib/_chunks/{_internalBrowser-Y0qKZ-GA.js → _internalBrowser-TWIhitgI.js} +22 -22
- package/lib/_chunks/_internalBrowser-TWIhitgI.js.map +1 -0
- package/lib/_chunks/{deployApiAction-SHteit1G.js → deployApiAction-TZcCtXan.js} +2 -2
- package/lib/_chunks/{deployApiAction-SHteit1G.js.map → deployApiAction-TZcCtXan.js.map} +1 -1
- package/lib/_chunks/{deployApiAction-bRyJpGOS.js → deployApiAction-rVL67VYW.js} +2 -2
- package/lib/_chunks/{deployApiAction-bRyJpGOS.js.map → deployApiAction-rVL67VYW.js.map} +1 -1
- package/lib/_chunks/{getStudioConfig-JSkc4GE0.js → getStudioWorkspaces-HX9o9-hl.js} +20 -4
- package/lib/_chunks/getStudioWorkspaces-HX9o9-hl.js.map +1 -0
- package/lib/_chunks/{index-EO9iRnDS.js → index-2kSxso3r.js} +2 -2
- package/lib/_chunks/{index-EO9iRnDS.js.map → index-2kSxso3r.js.map} +1 -1
- package/lib/_chunks/{index-FQCCBbuC.js → index-751ZLh3z.js} +6 -6
- package/lib/_chunks/{index-FQCCBbuC.js.map → index-751ZLh3z.js.map} +1 -1
- package/lib/_chunks/{index-VNSHvpZr.js → index-HcF369ru.js} +2 -2
- package/lib/_chunks/{index-VNSHvpZr.js.map → index-HcF369ru.js.map} +1 -1
- package/lib/_chunks/{index-Xs2xnLUV.js → index-MAAxgUnl.js} +6 -6
- package/lib/_chunks/{index-Xs2xnLUV.js.map → index-MAAxgUnl.js.map} +1 -1
- package/lib/_chunks/{index-AaK2CidU.js → index-NweJPfGb.js} +2 -2
- package/lib/_chunks/{index-AaK2CidU.js.map → index-NweJPfGb.js.map} +1 -1
- package/lib/_chunks/{index-R7R6AyHF.js → index-zobOqko7.js} +2 -2
- package/lib/_chunks/{index-R7R6AyHF.js.map → index-zobOqko7.js.map} +1 -1
- package/lib/_chunks/{listApisAction-6lGkFZrU.js → listApisAction-IvKV4iAd.js} +2 -2
- package/lib/_chunks/{listApisAction-6lGkFZrU.js.map → listApisAction-IvKV4iAd.js.map} +1 -1
- package/lib/_chunks/{listApisAction-CqCkBz-F.js → listApisAction-oca2uykY.js} +2 -2
- package/lib/_chunks/{listApisAction-CqCkBz-F.js.map → listApisAction-oca2uykY.js.map} +1 -1
- package/lib/_chunks/pane-9HEeITpx.js +5 -0
- package/lib/_chunks/pane-9HEeITpx.js.map +1 -0
- package/lib/_chunks/pane-QyVrOLqS.js +2 -0
- package/lib/_chunks/pane-QyVrOLqS.js.map +1 -0
- package/lib/_chunks/pane-TXXUUvdc.js +5 -0
- package/lib/_chunks/pane-TXXUUvdc.js.map +1 -0
- package/lib/_chunks/pane-cQxQtBcL.js +2 -0
- package/lib/_chunks/pane-cQxQtBcL.js.map +1 -0
- package/lib/_chunks/{structure-qJLnDJXq.js → structure-iqIDIH7-.js} +3 -3
- package/lib/_chunks/structure-iqIDIH7-.js.map +1 -0
- package/lib/_chunks/{structure-588eAwLd.js → structure-o_wMXC_G.js} +3 -3
- package/lib/_chunks/structure-o_wMXC_G.js.map +1 -0
- package/lib/_chunks/validateAction-4Jl_y_iB.js +154 -0
- package/lib/_chunks/validateAction-4Jl_y_iB.js.map +1 -0
- package/lib/_chunks/{validateAction-GUvMkXiN.js → validateAction-6q8Sqwaz.js} +25 -73
- package/lib/_chunks/validateAction-6q8Sqwaz.js.map +1 -0
- package/lib/_chunks/validateAction-Bz67ApRP.js +143 -0
- package/lib/_chunks/validateAction-Bz67ApRP.js.map +1 -0
- package/lib/_chunks/{validateAction-NIrqtyYJ.js → validateAction-shi462sq.js} +27 -75
- package/lib/_chunks/validateAction-shi462sq.js.map +1 -0
- package/lib/_internal/cli/threads/getGraphQLAPIs.js +2 -2
- package/lib/_internal/cli/threads/getGraphQLAPIs.js.map +1 -1
- package/lib/_internal/cli/threads/validateDocuments.js +5 -3
- package/lib/_internal/cli/threads/validateDocuments.js.map +1 -1
- package/lib/_internal/cli/threads/validateSchema.js +55 -0
- package/lib/_internal/cli/threads/validateSchema.js.map +1 -0
- package/lib/_internal.esm.js +1 -1
- package/lib/_internal.js +1 -1
- package/lib/_internalBrowser.esm.js +1 -1
- package/lib/_internalBrowser.js +1 -1
- package/lib/desk.esm.js +1 -1
- package/lib/desk.js +1 -1
- package/lib/exports/index.d.ts +25 -0
- package/lib/index.cjs.mjs +1 -0
- package/lib/index.esm.js +2 -2
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/structure.esm.js +1 -1
- package/lib/structure.js +1 -1
- package/package.json +15 -15
- package/src/_internal/cli/actions/schema/formatSchemaValidation.ts +96 -0
- package/src/_internal/cli/actions/schema/validateAction.ts +120 -0
- package/src/_internal/cli/actions/validation/reporters/prettyReporter/formatDocumentValidation.ts +17 -95
- package/src/_internal/cli/commands/index.ts +2 -0
- package/src/_internal/cli/commands/migration/createMigrationCommand.ts +18 -10
- package/src/_internal/cli/commands/migration/listMigrationsCommand.ts +34 -31
- package/src/_internal/cli/commands/migration/prettyMutationFormatter.ts +214 -0
- package/src/_internal/cli/commands/migration/runMigrationCommand.ts +102 -58
- package/src/_internal/cli/commands/migration/templates/minimalAdvanced.ts +1 -1
- package/src/_internal/cli/commands/migration/templates/minimalSimple.ts +1 -1
- package/src/_internal/cli/commands/migration/templates/renameField.ts +3 -3
- package/src/_internal/cli/commands/migration/templates/renameType.ts +1 -1
- package/src/_internal/cli/commands/migration/templates/stringToPTE.ts +1 -1
- package/src/_internal/cli/commands/migration/utils/mutationFormatter.ts +1 -1
- package/src/_internal/cli/commands/schema/validateSchemaCommand.ts +35 -0
- package/src/_internal/cli/threads/getGraphQLAPIs.ts +2 -2
- package/src/_internal/cli/threads/validateDocuments.ts +6 -3
- package/src/_internal/cli/threads/validateSchema.ts +73 -0
- package/src/_internal/cli/util/{getStudioConfig.ts → getStudioWorkspaces.ts} +30 -8
- package/src/_internal/cli/util/tree.ts +110 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/config/prepareConfig.ts +2 -5
- package/src/core/config/resolveSchemaTypes.ts +29 -0
- package/src/core/studio/screens/schemaErrors/SchemaProblemGroups.tsx +4 -2
- package/src/structure/comments/src/components/pte/blocks/MentionInlineBlock.tsx +13 -6
- package/lib/_chunks/_internal-2CJ5wSKF.js.map +0 -1
- package/lib/_chunks/_internal-79flWvL8.js.map +0 -1
- package/lib/_chunks/_internalBrowser-QAFz3SKp.js.map +0 -1
- package/lib/_chunks/_internalBrowser-Y0qKZ-GA.js.map +0 -1
- package/lib/_chunks/getStudioConfig-JSkc4GE0.js.map +0 -1
- package/lib/_chunks/pane-QmJb9ZTN.js +0 -2
- package/lib/_chunks/pane-QmJb9ZTN.js.map +0 -1
- package/lib/_chunks/pane-SK7FWNTJ.js +0 -2
- package/lib/_chunks/pane-SK7FWNTJ.js.map +0 -1
- package/lib/_chunks/pane-y4hpcKe1.js +0 -5
- package/lib/_chunks/pane-y4hpcKe1.js.map +0 -1
- package/lib/_chunks/pane-yQXBQyyI.js +0 -5
- package/lib/_chunks/pane-yQXBQyyI.js.map +0 -1
- package/lib/_chunks/structure-588eAwLd.js.map +0 -1
- package/lib/_chunks/structure-qJLnDJXq.js.map +0 -1
- package/lib/_chunks/validateAction-GUvMkXiN.js.map +0 -1
- package/lib/_chunks/validateAction-NIrqtyYJ.js.map +0 -1
@@ -0,0 +1,214 @@
|
|
1
|
+
import {isatty} from 'tty'
|
2
|
+
import {Migration, Mutation, NodePatch, Transaction} from '@sanity/migrate'
|
3
|
+
import {KeyedSegment} from '@sanity/types'
|
4
|
+
import {Chalk} from 'chalk'
|
5
|
+
import {convertToTree, formatTree, maxKeyLength} from '../../util/tree'
|
6
|
+
|
7
|
+
type ItemRef = string | number
|
8
|
+
type Impact = 'destructive' | 'maybeDestructive' | 'incremental'
|
9
|
+
type Variant = Impact | 'info'
|
10
|
+
|
11
|
+
const isTty = isatty(1)
|
12
|
+
|
13
|
+
interface FormatterOptions<Subject> {
|
14
|
+
chalk: Chalk
|
15
|
+
subject: Subject
|
16
|
+
migration: Migration
|
17
|
+
indentSize?: number
|
18
|
+
}
|
19
|
+
|
20
|
+
export function prettyFormat({
|
21
|
+
chalk,
|
22
|
+
subject,
|
23
|
+
migration,
|
24
|
+
indentSize = 0,
|
25
|
+
}: FormatterOptions<Mutation | Transaction | (Mutation | Transaction)[]>): string {
|
26
|
+
return (Array.isArray(subject) ? subject : [subject])
|
27
|
+
.map((subjectEntry) => {
|
28
|
+
if (subjectEntry.type === 'transaction') {
|
29
|
+
return [
|
30
|
+
[
|
31
|
+
badge('transaction', 'info', chalk),
|
32
|
+
typeof subjectEntry.id === 'undefined' ? null : chalk.underline(subjectEntry.id),
|
33
|
+
]
|
34
|
+
.filter(Boolean)
|
35
|
+
.join(' '),
|
36
|
+
indent(
|
37
|
+
prettyFormat({
|
38
|
+
chalk,
|
39
|
+
subject: subjectEntry.mutations,
|
40
|
+
migration,
|
41
|
+
indentSize: indentSize,
|
42
|
+
}),
|
43
|
+
),
|
44
|
+
].join('\n\n')
|
45
|
+
}
|
46
|
+
return prettyFormatMutation({
|
47
|
+
chalk,
|
48
|
+
subject: subjectEntry,
|
49
|
+
migration,
|
50
|
+
indentSize,
|
51
|
+
})
|
52
|
+
})
|
53
|
+
.join('\n\n')
|
54
|
+
}
|
55
|
+
|
56
|
+
function encodeItemRef(ref: number | KeyedSegment): ItemRef {
|
57
|
+
return typeof ref === 'number' ? ref : ref._key
|
58
|
+
}
|
59
|
+
|
60
|
+
function badgeStyle(chalk: Chalk, variant: Variant): Chalk {
|
61
|
+
const styles: Record<Variant, Chalk> = {
|
62
|
+
info: chalk.bgWhite.black,
|
63
|
+
incremental: chalk.bgGreen.black.bold,
|
64
|
+
maybeDestructive: chalk.bgYellow.black.bold,
|
65
|
+
destructive: chalk.bgRed.black.bold,
|
66
|
+
}
|
67
|
+
|
68
|
+
return styles[variant]
|
69
|
+
}
|
70
|
+
|
71
|
+
function badge(label: string, variant: Variant, chalk: Chalk): string {
|
72
|
+
if (!isTty) {
|
73
|
+
return `[${label}]`
|
74
|
+
}
|
75
|
+
|
76
|
+
return badgeStyle(chalk, variant)(` ${label} `)
|
77
|
+
}
|
78
|
+
|
79
|
+
const mutationImpact: Record<Mutation['type'], Impact> = {
|
80
|
+
create: 'incremental',
|
81
|
+
createIfNotExists: 'incremental',
|
82
|
+
createOrReplace: 'maybeDestructive',
|
83
|
+
delete: 'destructive',
|
84
|
+
patch: 'maybeDestructive',
|
85
|
+
}
|
86
|
+
|
87
|
+
function documentId(mutation: Mutation): string | undefined {
|
88
|
+
if ('id' in mutation) {
|
89
|
+
return mutation.id
|
90
|
+
}
|
91
|
+
|
92
|
+
if ('document' in mutation) {
|
93
|
+
return mutation.document._id
|
94
|
+
}
|
95
|
+
|
96
|
+
return undefined
|
97
|
+
}
|
98
|
+
|
99
|
+
const listFormatter = new Intl.ListFormat('en-US', {
|
100
|
+
type: 'disjunction',
|
101
|
+
})
|
102
|
+
|
103
|
+
function mutationHeader(chalk: Chalk, mutation: Mutation, migration: Migration): string {
|
104
|
+
const mutationType = badge(mutation.type, mutationImpact[mutation.type], chalk)
|
105
|
+
|
106
|
+
const documentType =
|
107
|
+
'document' in mutation || migration.documentTypes
|
108
|
+
? badge(
|
109
|
+
'document' in mutation
|
110
|
+
? mutation.document._type
|
111
|
+
: listFormatter.format(migration.documentTypes ?? []),
|
112
|
+
'info',
|
113
|
+
chalk,
|
114
|
+
)
|
115
|
+
: null
|
116
|
+
|
117
|
+
// TODO: Should we list documentType when a mutation can be yielded for any document type?
|
118
|
+
return [mutationType, documentType, chalk.underline(documentId(mutation))]
|
119
|
+
.filter(Boolean)
|
120
|
+
.join(' ')
|
121
|
+
}
|
122
|
+
|
123
|
+
export function prettyFormatMutation({
|
124
|
+
chalk,
|
125
|
+
subject,
|
126
|
+
migration,
|
127
|
+
indentSize = 0,
|
128
|
+
}: FormatterOptions<Mutation>): string {
|
129
|
+
const lock =
|
130
|
+
'options' in subject ? chalk.cyan(`(if revision==${subject.options?.ifRevision})`) : ''
|
131
|
+
const header = [mutationHeader(chalk, subject, migration), lock].join(' ')
|
132
|
+
const padding = ' '.repeat(indentSize)
|
133
|
+
|
134
|
+
if (
|
135
|
+
subject.type === 'create' ||
|
136
|
+
subject.type === 'createIfNotExists' ||
|
137
|
+
subject.type === 'createOrReplace'
|
138
|
+
) {
|
139
|
+
return [header, '\n', indent(JSON.stringify(subject.document, null, 2), indentSize)].join('')
|
140
|
+
}
|
141
|
+
|
142
|
+
if (subject.type === 'patch') {
|
143
|
+
const tree = convertToTree<NodePatch>(subject.patches.flat())
|
144
|
+
const paddingLength = Math.max(maxKeyLength(tree.children) + 2, 30)
|
145
|
+
|
146
|
+
return [
|
147
|
+
header,
|
148
|
+
'\n',
|
149
|
+
formatTree<NodePatch>({
|
150
|
+
node: tree.children,
|
151
|
+
paddingLength,
|
152
|
+
indent: padding,
|
153
|
+
getMessage: (patch) => formatPatchMutation(chalk, patch),
|
154
|
+
}),
|
155
|
+
].join('')
|
156
|
+
}
|
157
|
+
|
158
|
+
return header
|
159
|
+
}
|
160
|
+
|
161
|
+
function formatPatchMutation(chalk: Chalk, patch: NodePatch): string {
|
162
|
+
const {op} = patch
|
163
|
+
const formattedType = chalk.bold(op.type)
|
164
|
+
if (op.type === 'unset') {
|
165
|
+
return `${chalk.red(formattedType)}()`
|
166
|
+
}
|
167
|
+
if (op.type === 'diffMatchPatch') {
|
168
|
+
return `${chalk.yellow(formattedType)}(${op.value})`
|
169
|
+
}
|
170
|
+
if (op.type === 'inc' || op.type === 'dec') {
|
171
|
+
return `${chalk.yellow(formattedType)}(${op.amount})`
|
172
|
+
}
|
173
|
+
if (op.type === 'set') {
|
174
|
+
return `${chalk.yellow(formattedType)}(${JSON.stringify(op.value)})`
|
175
|
+
}
|
176
|
+
if (op.type === 'setIfMissing') {
|
177
|
+
return `${chalk.green(formattedType)}(${JSON.stringify(op.value)})`
|
178
|
+
}
|
179
|
+
if (op.type === 'assign') {
|
180
|
+
return `${chalk.yellow(formattedType)}(${JSON.stringify(op.value)})`
|
181
|
+
}
|
182
|
+
if (op.type === 'unassign') {
|
183
|
+
return `${chalk.red(formattedType)}(${JSON.stringify(op.keys)})`
|
184
|
+
}
|
185
|
+
if (op.type === 'insert') {
|
186
|
+
return `${chalk.green(formattedType)}(${op.position}, ${encodeItemRef(
|
187
|
+
op.referenceItem,
|
188
|
+
)}, ${JSON.stringify(op.items)})`
|
189
|
+
}
|
190
|
+
if (op.type === 'upsert') {
|
191
|
+
return `${chalk.yellow(formattedType)}(${op.position}, ${encodeItemRef(
|
192
|
+
op.referenceItem,
|
193
|
+
)}, ${JSON.stringify(op.items)})`
|
194
|
+
}
|
195
|
+
if (op.type === 'replace') {
|
196
|
+
return `${chalk.yellow(formattedType)}(${encodeItemRef(op.referenceItem)}, ${JSON.stringify(
|
197
|
+
op.items,
|
198
|
+
)})`
|
199
|
+
}
|
200
|
+
if (op.type === 'truncate') {
|
201
|
+
return `${chalk.red(formattedType)}(${op.startIndex}, ${op.endIndex})`
|
202
|
+
}
|
203
|
+
// @ts-expect-error all cases are covered
|
204
|
+
throw new Error(`Invalid operation type: ${op.type}`)
|
205
|
+
}
|
206
|
+
|
207
|
+
function indent(subject: string, size = 2): string {
|
208
|
+
const padding = ' '.repeat(size)
|
209
|
+
|
210
|
+
return subject
|
211
|
+
.split('\n')
|
212
|
+
.map((line) => padding + line)
|
213
|
+
.join('\n')
|
214
|
+
}
|
@@ -11,29 +11,31 @@ import {
|
|
11
11
|
runFromArchive,
|
12
12
|
} from '@sanity/migrate'
|
13
13
|
|
14
|
+
import {Table} from 'console-table-printer'
|
14
15
|
import {debug} from '../../debug'
|
15
|
-
import {
|
16
|
+
import {resolveMigrations} from './listMigrationsCommand'
|
17
|
+
import {prettyFormat} from './prettyMutationFormatter'
|
16
18
|
|
17
19
|
const helpText = `
|
18
20
|
Options
|
19
21
|
--dry <boolean> Whether or not to dry run the migration. Default to true, to actually run the migration, pass --no-dry
|
20
|
-
--from-export <export.tar.gz> Use a local dataset export as source for migration instead of calling the Sanity API. Note: this is only supported for dry runs.
|
21
22
|
--concurrency <concurrent> How many mutation requests to run in parallel. Must be between 1 and ${MAX_MUTATION_CONCURRENCY}. Default: ${DEFAULT_MUTATION_CONCURRENCY}.
|
22
23
|
--no-progress Don't output progress. Useful if you want debug your migration script and see the output of console.log() statements.
|
23
24
|
--dataset <dataset> Dataset to migrate. Defaults to the dataset configured in your Sanity CLI config.
|
24
25
|
--projectId <project id> Project ID of the dataset to migrate. Defaults to the projectId configured in your Sanity CLI config.
|
25
26
|
--no-confirm Skip the confirmation prompt before running the migration. Make sure you know what you're doing before using this flag.
|
27
|
+
--from-export <export.tar.gz> Use a local dataset export as source for migration instead of calling the Sanity API. Note: this is only supported for dry runs.
|
26
28
|
|
27
29
|
|
28
30
|
Examples
|
29
31
|
# dry run the migration
|
30
|
-
sanity migration run <
|
32
|
+
sanity migration run <id>
|
31
33
|
|
32
34
|
# execute the migration against a dataset
|
33
|
-
sanity migration run <
|
35
|
+
sanity migration run <id> --no-dry --projectId xyz --dataset staging
|
34
36
|
|
35
37
|
# run the migration using the dataset export as the source
|
36
|
-
sanity migration run <
|
38
|
+
sanity migration run <id> --dry false --from-export=production.tar.gz --projectId xyz --dataset staging
|
37
39
|
`
|
38
40
|
|
39
41
|
interface CreateFlags {
|
@@ -48,8 +50,8 @@ interface CreateFlags {
|
|
48
50
|
|
49
51
|
const tryExtensions = ['mjs', 'js', 'ts', 'cjs']
|
50
52
|
|
51
|
-
function resolveMigrationScript(workDir: string,
|
52
|
-
return [
|
53
|
+
function resolveMigrationScript(workDir: string, migrationId: string) {
|
54
|
+
return [migrationId, path.join(migrationId, 'index')].flatMap((location) =>
|
53
55
|
tryExtensions.map((ext) => {
|
54
56
|
const relativePath = path.join('migrations', `${location}.${ext}`)
|
55
57
|
const absolutePath = path.resolve(workDir, relativePath)
|
@@ -58,22 +60,24 @@ function resolveMigrationScript(workDir: string, migrationName: string) {
|
|
58
60
|
// eslint-disable-next-line import/no-dynamic-require
|
59
61
|
mod = require(absolutePath)
|
60
62
|
} catch (err) {
|
61
|
-
|
63
|
+
if (err.code !== 'MODULE_NOT_FOUND') {
|
64
|
+
throw new Error(`Error: ${err.message}"`)
|
65
|
+
}
|
62
66
|
}
|
63
67
|
return {relativePath, absolutePath, mod}
|
64
68
|
}),
|
65
69
|
)
|
66
70
|
}
|
67
71
|
|
68
|
-
const
|
72
|
+
const runMigrationCommand: CliCommandDefinition<CreateFlags> = {
|
69
73
|
name: 'run',
|
70
74
|
group: 'migration',
|
71
|
-
signature: '
|
75
|
+
signature: 'ID',
|
72
76
|
helpText,
|
73
77
|
description: 'Run a migration against a dataset',
|
74
78
|
action: async (args, context) => {
|
75
79
|
const {apiClient, output, prompt, chalk, workDir} = context
|
76
|
-
const [
|
80
|
+
const [id] = args.argsWithoutOptions
|
77
81
|
|
78
82
|
const [fromExport, dry, showProgress, dataset, projectId, shouldConfirm] = [
|
79
83
|
args.extOptions['from-export'],
|
@@ -88,8 +92,24 @@ const createMigrationCommand: CliCommandDefinition<CreateFlags> = {
|
|
88
92
|
throw new Error('If either --dataset or --projectId is provided, both must be provided')
|
89
93
|
}
|
90
94
|
|
91
|
-
if (!
|
92
|
-
|
95
|
+
if (!id) {
|
96
|
+
output.error(chalk.red('Error: Migration ID must be provided'))
|
97
|
+
const migrations = await resolveMigrations(workDir)
|
98
|
+
const table = new Table({
|
99
|
+
title: `Migrations found in project`,
|
100
|
+
columns: [
|
101
|
+
{name: 'id', title: 'ID', alignment: 'left'},
|
102
|
+
{name: 'title', title: 'Title', alignment: 'left'},
|
103
|
+
],
|
104
|
+
})
|
105
|
+
|
106
|
+
migrations.forEach((definedMigration) => {
|
107
|
+
table.addRow({id: definedMigration.dirname, title: definedMigration.migration.title})
|
108
|
+
})
|
109
|
+
table.printTable()
|
110
|
+
output.print('\nRun `sanity migration run <ID>` to run a migration')
|
111
|
+
|
112
|
+
return
|
93
113
|
}
|
94
114
|
|
95
115
|
if (!__DEV__) {
|
@@ -98,21 +118,21 @@ const createMigrationCommand: CliCommandDefinition<CreateFlags> = {
|
|
98
118
|
})
|
99
119
|
}
|
100
120
|
|
101
|
-
const candidates = resolveMigrationScript(workDir,
|
121
|
+
const candidates = resolveMigrationScript(workDir, id)
|
102
122
|
|
103
123
|
const resolvedScripts = candidates.filter((candidate) => candidate!.mod?.default)
|
104
124
|
|
105
125
|
if (resolvedScripts.length > 1) {
|
106
126
|
// todo: consider prompt user about which one to run? note: it's likely a mistake if multiple files resolve to the same name
|
107
127
|
throw new Error(
|
108
|
-
`Found multiple migrations for "${
|
128
|
+
`Found multiple migrations for "${id}" in current directory ${candidates
|
109
129
|
.map((candidate) => candidate!.relativePath)
|
110
130
|
.join(', ')}`,
|
111
131
|
)
|
112
132
|
}
|
113
133
|
if (resolvedScripts.length === 0) {
|
114
134
|
throw new Error(
|
115
|
-
`No migration found for "${
|
135
|
+
`No migration found for "${id}" in current directory. Make sure that the migration file exists and exports a valid migration as its default export.\n
|
116
136
|
Tried the following files:\n -${candidates
|
117
137
|
.map((candidate) => candidate.relativePath)
|
118
138
|
.join('\n - ')}`,
|
@@ -161,65 +181,53 @@ const createMigrationCommand: CliCommandDefinition<CreateFlags> = {
|
|
161
181
|
token: projectConfig.token!,
|
162
182
|
apiVersion: 'v2024-01-29',
|
163
183
|
} as const
|
164
|
-
|
165
184
|
if (dry) {
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
api: apiConfig,
|
170
|
-
concurrency,
|
171
|
-
onProgress: createProgress(spinner),
|
172
|
-
})
|
173
|
-
return
|
174
|
-
}
|
175
|
-
|
176
|
-
dryRun({api: apiConfig}, migration)
|
177
|
-
|
178
|
-
spinner.stop()
|
179
|
-
} else {
|
180
|
-
const response =
|
181
|
-
shouldConfirm &&
|
182
|
-
(await prompt.single<boolean>({
|
183
|
-
message: `This migration will run on the ${chalk.yellow(
|
184
|
-
chalk.bold(apiConfig.dataset),
|
185
|
-
)} dataset in ${chalk.yellow(chalk.bold(apiConfig.projectId))} project. Are you sure?`,
|
186
|
-
type: 'confirm',
|
187
|
-
}))
|
188
|
-
|
189
|
-
if (response === false) {
|
190
|
-
debug('User aborted migration')
|
191
|
-
return
|
192
|
-
}
|
185
|
+
dryRunHandler()
|
186
|
+
return
|
187
|
+
}
|
193
188
|
|
194
|
-
|
195
|
-
|
196
|
-
|
189
|
+
const response =
|
190
|
+
shouldConfirm &&
|
191
|
+
(await prompt.single<boolean>({
|
192
|
+
message: `This migration will run on the ${chalk.yellow(
|
193
|
+
chalk.bold(apiConfig.dataset),
|
194
|
+
)} dataset in ${chalk.yellow(chalk.bold(apiConfig.projectId))} project. Are you sure?`,
|
195
|
+
type: 'confirm',
|
196
|
+
}))
|
197
|
+
|
198
|
+
if (response === false) {
|
199
|
+
debug('User aborted migration')
|
200
|
+
return
|
197
201
|
}
|
198
202
|
|
199
|
-
|
203
|
+
const spinner = output.spinner(`Running migration "${id}"`).start()
|
204
|
+
await run({api: apiConfig, concurrency, onProgress: createProgress(spinner)}, migration)
|
205
|
+
spinner.stop()
|
206
|
+
|
207
|
+
function createProgress(progressSpinner: ReturnType<typeof output.spinner>) {
|
200
208
|
return function onProgress(progress: MigrationProgress) {
|
201
209
|
if (!showProgress) {
|
202
|
-
|
210
|
+
progressSpinner.stop()
|
203
211
|
return
|
204
212
|
}
|
205
213
|
if (progress.done) {
|
206
|
-
|
214
|
+
progressSpinner.text = `Migration "${id}" completed.
|
207
215
|
|
208
|
-
Project id: ${chalk.bold(projectId)}
|
209
|
-
Dataset: ${chalk.bold(dataset)}
|
216
|
+
Project id: ${chalk.bold(apiConfig.projectId)}
|
217
|
+
Dataset: ${chalk.bold(apiConfig.dataset)}
|
210
218
|
|
211
219
|
${progress.documents} documents processed.
|
212
220
|
${progress.mutations} mutations generated.
|
213
221
|
${chalk.green(progress.completedTransactions.length)} transactions committed.`
|
214
|
-
|
222
|
+
progressSpinner.stopAndPersist({symbol: chalk.green('✔')})
|
215
223
|
return
|
216
224
|
}
|
217
225
|
|
218
226
|
;[null, ...progress.currentTransactions].forEach((transaction) => {
|
219
|
-
|
227
|
+
progressSpinner.text = `Running migration "${id}" ${dry ? 'in dry mode...' : '...'}
|
220
228
|
|
221
|
-
Project id: ${chalk.bold(projectId)}
|
222
|
-
Dataset: ${chalk.bold(dataset)}
|
229
|
+
Project id: ${chalk.bold(apiConfig.projectId)}
|
230
|
+
Dataset: ${chalk.bold(apiConfig.dataset)}
|
223
231
|
Document type: ${chalk.bold(migration.documentTypes?.join(','))}
|
224
232
|
|
225
233
|
${progress.documents} documents processed…
|
@@ -227,11 +235,47 @@ const createMigrationCommand: CliCommandDefinition<CreateFlags> = {
|
|
227
235
|
${chalk.blue(progress.pending)} requests pending…
|
228
236
|
${chalk.green(progress.completedTransactions.length)} transactions committed.
|
229
237
|
|
230
|
-
${
|
238
|
+
${
|
239
|
+
transaction && !progress.done
|
240
|
+
? `» ${prettyFormat({chalk, subject: transaction, migration, indentSize: 2})}`
|
241
|
+
: ''
|
242
|
+
}`
|
243
|
+
})
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
async function dryRunHandler() {
|
248
|
+
if (fromExport) {
|
249
|
+
const dryRunSpinner = output.spinner(`Running migration "${id}" in dry mode`).start()
|
250
|
+
|
251
|
+
// TODO: Dry run output when using archive source.
|
252
|
+
await runFromArchive(migration, fromExport, {
|
253
|
+
api: apiConfig,
|
254
|
+
concurrency,
|
255
|
+
onProgress: createProgress(dryRunSpinner),
|
231
256
|
})
|
257
|
+
|
258
|
+
dryRunSpinner.stop()
|
259
|
+
} else {
|
260
|
+
output.print(`Running migration "${id}" in dry mode`)
|
261
|
+
output.print()
|
262
|
+
output.print(`Project id: ${chalk.bold(apiConfig.projectId)}`)
|
263
|
+
output.print(`Dataset: ${chalk.bold(apiConfig.dataset)}`)
|
264
|
+
|
265
|
+
for await (const mutation of dryRun({api: apiConfig}, migration)) {
|
266
|
+
if (!mutation) continue
|
267
|
+
output.print()
|
268
|
+
output.print(
|
269
|
+
prettyFormat({
|
270
|
+
chalk,
|
271
|
+
subject: mutation,
|
272
|
+
migration,
|
273
|
+
}),
|
274
|
+
)
|
275
|
+
}
|
232
276
|
}
|
233
277
|
}
|
234
278
|
},
|
235
279
|
}
|
236
280
|
|
237
|
-
export default
|
281
|
+
export default runMigrationCommand
|
@@ -11,7 +11,7 @@ export const minimalAdvanced = ({
|
|
11
11
|
* and make \`true\` the default value for the \`enabled\` field
|
12
12
|
*/
|
13
13
|
export default defineMigration({
|
14
|
-
|
14
|
+
title: '${migrationName}',
|
15
15
|
${
|
16
16
|
documentTypes.length > 0
|
17
17
|
? ` documentTypes: [${documentTypes.map((t) => JSON.stringify(t)).join(', ')}],\n`
|
@@ -7,7 +7,7 @@ export const minimalSimple = ({
|
|
7
7
|
}) => `import {defineMigration} from 'sanity/migrate'
|
8
8
|
|
9
9
|
export default defineMigration({
|
10
|
-
|
10
|
+
title: '${migrationName}',
|
11
11
|
${
|
12
12
|
documentTypes.length > 0
|
13
13
|
? ` documentTypes: [${documentTypes.map((t) => JSON.stringify(t)).join(', ')}],\n`
|
@@ -10,16 +10,16 @@ const from = 'oldFieldName'
|
|
10
10
|
const to = 'newFieldName'
|
11
11
|
|
12
12
|
export default defineMigration({
|
13
|
-
|
13
|
+
title: '${migrationName}',
|
14
14
|
${
|
15
15
|
documentTypes.length > 0
|
16
16
|
? ` documentTypes: [${documentTypes.map((t) => JSON.stringify(t)).join(', ')}],\n`
|
17
17
|
: ''
|
18
18
|
}
|
19
19
|
migrate: {
|
20
|
-
document(doc,
|
20
|
+
document(doc, context) {
|
21
21
|
return [
|
22
|
-
at(to, setIfMissing(doc[from]))
|
22
|
+
at(to, setIfMissing(doc[from])),
|
23
23
|
at(from, unset())
|
24
24
|
]
|
25
25
|
}
|
@@ -10,7 +10,7 @@ const oldType = 'old'
|
|
10
10
|
const newType = 'new'
|
11
11
|
|
12
12
|
export default defineMigration({
|
13
|
-
|
13
|
+
title: '${migrationName}',
|
14
14
|
${
|
15
15
|
documentTypes.length > 0
|
16
16
|
? ` documentTypes: [${documentTypes.map((t) => JSON.stringify(t)).join(', ')}],\n`
|
@@ -10,7 +10,7 @@ import {defineMigration, set} from 'sanity/migrate'
|
|
10
10
|
const targetPath = stringToPath('some.path')
|
11
11
|
|
12
12
|
export default defineMigration({
|
13
|
-
|
13
|
+
title: '${migrationName}',
|
14
14
|
${
|
15
15
|
documentTypes.length > 0
|
16
16
|
? ` documentTypes: [${documentTypes.map((t) => JSON.stringify(t)).join(', ')}],\n`
|
@@ -83,7 +83,7 @@ function formatPatchMutation(chalk: Chalk, patch: NodePatch<any>): string {
|
|
83
83
|
].join(': ')
|
84
84
|
}
|
85
85
|
if (op.type === 'truncate') {
|
86
|
-
return [path, `${formattedType}(${op.startIndex}, ${op.endIndex}`].join(': ')
|
86
|
+
return [path, `${formattedType}(${op.startIndex}, ${op.endIndex})`].join(': ')
|
87
87
|
}
|
88
88
|
// @ts-expect-error all cases are covered
|
89
89
|
throw new Error(`Invalid operation type: ${op.type}`)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import type {CliCommandDefinition} from '@sanity/cli'
|
2
|
+
|
3
|
+
const description = 'Validates all schema types specified in a workspace.'
|
4
|
+
|
5
|
+
const helpText = `
|
6
|
+
Options
|
7
|
+
--workspace <name> The name of the workspace to use when validating all schema types.
|
8
|
+
--format <pretty|ndjson|json> The output format used to print schema errors and warnings.
|
9
|
+
--level <error|warning> The minimum level reported out. Defaults to warning.
|
10
|
+
|
11
|
+
Examples
|
12
|
+
# Validates all schema types in a Sanity project with more than one workspace
|
13
|
+
sanity schema validate --workspace default
|
14
|
+
|
15
|
+
# Save the results of the report into a file
|
16
|
+
sanity schema validate > report.txt
|
17
|
+
|
18
|
+
# Report out only errors
|
19
|
+
sanity schema validate --level error
|
20
|
+
`
|
21
|
+
|
22
|
+
const validateDocumentsCommand: CliCommandDefinition = {
|
23
|
+
name: 'validate',
|
24
|
+
group: 'schema',
|
25
|
+
signature: '',
|
26
|
+
description,
|
27
|
+
helpText,
|
28
|
+
action: async (args, context) => {
|
29
|
+
const mod = await import('../../actions/schema/validateAction')
|
30
|
+
|
31
|
+
return mod.default(args, context)
|
32
|
+
},
|
33
|
+
} satisfies CliCommandDefinition
|
34
|
+
|
35
|
+
export default validateDocumentsCommand
|
@@ -4,7 +4,7 @@ import {isPlainObject} from 'lodash'
|
|
4
4
|
import type {Schema} from '@sanity/types'
|
5
5
|
import type {CliV3CommandContext, GraphQLAPIConfig} from '@sanity/cli'
|
6
6
|
import type {SchemaDefinitionish, TypeResolvedGraphQLAPI} from '../actions/graphql/types'
|
7
|
-
import {
|
7
|
+
import {getStudioWorkspaces} from '../util/getStudioWorkspaces'
|
8
8
|
import {Workspace} from 'sanity'
|
9
9
|
|
10
10
|
if (isMainThread || !parentPort) {
|
@@ -26,7 +26,7 @@ async function resolveGraphQLApis({
|
|
26
26
|
}: Pick<CliV3CommandContext, 'cliConfig' | 'cliConfigPath' | 'workDir'>): Promise<
|
27
27
|
TypeResolvedGraphQLAPI[]
|
28
28
|
> {
|
29
|
-
const workspaces = await
|
29
|
+
const workspaces = await getStudioWorkspaces({basePath: workDir})
|
30
30
|
const numSources = workspaces.reduce(
|
31
31
|
(count, workspace) => count + workspace.unstable_sources.length,
|
32
32
|
0,
|
@@ -11,7 +11,7 @@ import {
|
|
11
11
|
createClient,
|
12
12
|
} from '@sanity/client'
|
13
13
|
import {type ValidationContext, type ValidationMarker, isReference} from '@sanity/types'
|
14
|
-
import {
|
14
|
+
import {getStudioWorkspaces} from '../util/getStudioWorkspaces'
|
15
15
|
import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment'
|
16
16
|
import {
|
17
17
|
createReporter,
|
@@ -108,7 +108,10 @@ const idRegex = /^[^-][A-Z0-9._-]*$/i
|
|
108
108
|
|
109
109
|
// during testing, the `doc` endpoint 502'ed if given an invalid ID
|
110
110
|
const isValidId = (id: unknown) => typeof id === 'string' && idRegex.test(id)
|
111
|
-
const shouldIncludeDocument = (document: SanityDocument) =>
|
111
|
+
const shouldIncludeDocument = (document: SanityDocument) => {
|
112
|
+
// Filter out system documents
|
113
|
+
return !document._type.startsWith('system.')
|
114
|
+
}
|
112
115
|
|
113
116
|
async function* readerToGenerator(reader: ReadableStreamDefaultReader<Uint8Array>) {
|
114
117
|
while (true) {
|
@@ -121,7 +124,7 @@ async function* readerToGenerator(reader: ReadableStreamDefaultReader<Uint8Array
|
|
121
124
|
validateDocuments()
|
122
125
|
|
123
126
|
async function loadWorkspace() {
|
124
|
-
const workspaces = await
|
127
|
+
const workspaces = await getStudioWorkspaces({basePath: workDir, configPath})
|
125
128
|
|
126
129
|
if (!workspaces.length) {
|
127
130
|
throw new Error(`Configuration did not return any workspaces.`)
|