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.
Files changed (112) hide show
  1. package/lib/_chunks/{_internal-2CJ5wSKF.js → _internal-6Pl2wJGj.js} +422 -201
  2. package/lib/_chunks/_internal-6Pl2wJGj.js.map +1 -0
  3. package/lib/_chunks/{_internal-79flWvL8.js → _internal-yQwMw2ht.js} +419 -203
  4. package/lib/_chunks/_internal-yQwMw2ht.js.map +1 -0
  5. package/lib/_chunks/{_internalBrowser-QAFz3SKp.js → _internalBrowser-HWvRXvlU.js} +22 -22
  6. package/lib/_chunks/_internalBrowser-HWvRXvlU.js.map +1 -0
  7. package/lib/_chunks/{_internalBrowser-Y0qKZ-GA.js → _internalBrowser-TWIhitgI.js} +22 -22
  8. package/lib/_chunks/_internalBrowser-TWIhitgI.js.map +1 -0
  9. package/lib/_chunks/{deployApiAction-SHteit1G.js → deployApiAction-TZcCtXan.js} +2 -2
  10. package/lib/_chunks/{deployApiAction-SHteit1G.js.map → deployApiAction-TZcCtXan.js.map} +1 -1
  11. package/lib/_chunks/{deployApiAction-bRyJpGOS.js → deployApiAction-rVL67VYW.js} +2 -2
  12. package/lib/_chunks/{deployApiAction-bRyJpGOS.js.map → deployApiAction-rVL67VYW.js.map} +1 -1
  13. package/lib/_chunks/{getStudioConfig-JSkc4GE0.js → getStudioWorkspaces-HX9o9-hl.js} +20 -4
  14. package/lib/_chunks/getStudioWorkspaces-HX9o9-hl.js.map +1 -0
  15. package/lib/_chunks/{index-EO9iRnDS.js → index-2kSxso3r.js} +2 -2
  16. package/lib/_chunks/{index-EO9iRnDS.js.map → index-2kSxso3r.js.map} +1 -1
  17. package/lib/_chunks/{index-FQCCBbuC.js → index-751ZLh3z.js} +6 -6
  18. package/lib/_chunks/{index-FQCCBbuC.js.map → index-751ZLh3z.js.map} +1 -1
  19. package/lib/_chunks/{index-VNSHvpZr.js → index-HcF369ru.js} +2 -2
  20. package/lib/_chunks/{index-VNSHvpZr.js.map → index-HcF369ru.js.map} +1 -1
  21. package/lib/_chunks/{index-Xs2xnLUV.js → index-MAAxgUnl.js} +6 -6
  22. package/lib/_chunks/{index-Xs2xnLUV.js.map → index-MAAxgUnl.js.map} +1 -1
  23. package/lib/_chunks/{index-AaK2CidU.js → index-NweJPfGb.js} +2 -2
  24. package/lib/_chunks/{index-AaK2CidU.js.map → index-NweJPfGb.js.map} +1 -1
  25. package/lib/_chunks/{index-R7R6AyHF.js → index-zobOqko7.js} +2 -2
  26. package/lib/_chunks/{index-R7R6AyHF.js.map → index-zobOqko7.js.map} +1 -1
  27. package/lib/_chunks/{listApisAction-6lGkFZrU.js → listApisAction-IvKV4iAd.js} +2 -2
  28. package/lib/_chunks/{listApisAction-6lGkFZrU.js.map → listApisAction-IvKV4iAd.js.map} +1 -1
  29. package/lib/_chunks/{listApisAction-CqCkBz-F.js → listApisAction-oca2uykY.js} +2 -2
  30. package/lib/_chunks/{listApisAction-CqCkBz-F.js.map → listApisAction-oca2uykY.js.map} +1 -1
  31. package/lib/_chunks/pane-9HEeITpx.js +5 -0
  32. package/lib/_chunks/pane-9HEeITpx.js.map +1 -0
  33. package/lib/_chunks/pane-QyVrOLqS.js +2 -0
  34. package/lib/_chunks/pane-QyVrOLqS.js.map +1 -0
  35. package/lib/_chunks/pane-TXXUUvdc.js +5 -0
  36. package/lib/_chunks/pane-TXXUUvdc.js.map +1 -0
  37. package/lib/_chunks/pane-cQxQtBcL.js +2 -0
  38. package/lib/_chunks/pane-cQxQtBcL.js.map +1 -0
  39. package/lib/_chunks/{structure-qJLnDJXq.js → structure-iqIDIH7-.js} +3 -3
  40. package/lib/_chunks/structure-iqIDIH7-.js.map +1 -0
  41. package/lib/_chunks/{structure-588eAwLd.js → structure-o_wMXC_G.js} +3 -3
  42. package/lib/_chunks/structure-o_wMXC_G.js.map +1 -0
  43. package/lib/_chunks/validateAction-4Jl_y_iB.js +154 -0
  44. package/lib/_chunks/validateAction-4Jl_y_iB.js.map +1 -0
  45. package/lib/_chunks/{validateAction-GUvMkXiN.js → validateAction-6q8Sqwaz.js} +25 -73
  46. package/lib/_chunks/validateAction-6q8Sqwaz.js.map +1 -0
  47. package/lib/_chunks/validateAction-Bz67ApRP.js +143 -0
  48. package/lib/_chunks/validateAction-Bz67ApRP.js.map +1 -0
  49. package/lib/_chunks/{validateAction-NIrqtyYJ.js → validateAction-shi462sq.js} +27 -75
  50. package/lib/_chunks/validateAction-shi462sq.js.map +1 -0
  51. package/lib/_internal/cli/threads/getGraphQLAPIs.js +2 -2
  52. package/lib/_internal/cli/threads/getGraphQLAPIs.js.map +1 -1
  53. package/lib/_internal/cli/threads/validateDocuments.js +5 -3
  54. package/lib/_internal/cli/threads/validateDocuments.js.map +1 -1
  55. package/lib/_internal/cli/threads/validateSchema.js +55 -0
  56. package/lib/_internal/cli/threads/validateSchema.js.map +1 -0
  57. package/lib/_internal.esm.js +1 -1
  58. package/lib/_internal.js +1 -1
  59. package/lib/_internalBrowser.esm.js +1 -1
  60. package/lib/_internalBrowser.js +1 -1
  61. package/lib/desk.esm.js +1 -1
  62. package/lib/desk.js +1 -1
  63. package/lib/exports/index.d.ts +25 -0
  64. package/lib/index.cjs.mjs +1 -0
  65. package/lib/index.esm.js +2 -2
  66. package/lib/index.js +2 -1
  67. package/lib/index.js.map +1 -1
  68. package/lib/structure.esm.js +1 -1
  69. package/lib/structure.js +1 -1
  70. package/package.json +15 -15
  71. package/src/_internal/cli/actions/schema/formatSchemaValidation.ts +96 -0
  72. package/src/_internal/cli/actions/schema/validateAction.ts +120 -0
  73. package/src/_internal/cli/actions/validation/reporters/prettyReporter/formatDocumentValidation.ts +17 -95
  74. package/src/_internal/cli/commands/index.ts +2 -0
  75. package/src/_internal/cli/commands/migration/createMigrationCommand.ts +18 -10
  76. package/src/_internal/cli/commands/migration/listMigrationsCommand.ts +34 -31
  77. package/src/_internal/cli/commands/migration/prettyMutationFormatter.ts +214 -0
  78. package/src/_internal/cli/commands/migration/runMigrationCommand.ts +102 -58
  79. package/src/_internal/cli/commands/migration/templates/minimalAdvanced.ts +1 -1
  80. package/src/_internal/cli/commands/migration/templates/minimalSimple.ts +1 -1
  81. package/src/_internal/cli/commands/migration/templates/renameField.ts +3 -3
  82. package/src/_internal/cli/commands/migration/templates/renameType.ts +1 -1
  83. package/src/_internal/cli/commands/migration/templates/stringToPTE.ts +1 -1
  84. package/src/_internal/cli/commands/migration/utils/mutationFormatter.ts +1 -1
  85. package/src/_internal/cli/commands/schema/validateSchemaCommand.ts +35 -0
  86. package/src/_internal/cli/threads/getGraphQLAPIs.ts +2 -2
  87. package/src/_internal/cli/threads/validateDocuments.ts +6 -3
  88. package/src/_internal/cli/threads/validateSchema.ts +73 -0
  89. package/src/_internal/cli/util/{getStudioConfig.ts → getStudioWorkspaces.ts} +30 -8
  90. package/src/_internal/cli/util/tree.ts +110 -0
  91. package/src/core/config/index.ts +1 -0
  92. package/src/core/config/prepareConfig.ts +2 -5
  93. package/src/core/config/resolveSchemaTypes.ts +29 -0
  94. package/src/core/studio/screens/schemaErrors/SchemaProblemGroups.tsx +4 -2
  95. package/src/structure/comments/src/components/pte/blocks/MentionInlineBlock.tsx +13 -6
  96. package/lib/_chunks/_internal-2CJ5wSKF.js.map +0 -1
  97. package/lib/_chunks/_internal-79flWvL8.js.map +0 -1
  98. package/lib/_chunks/_internalBrowser-QAFz3SKp.js.map +0 -1
  99. package/lib/_chunks/_internalBrowser-Y0qKZ-GA.js.map +0 -1
  100. package/lib/_chunks/getStudioConfig-JSkc4GE0.js.map +0 -1
  101. package/lib/_chunks/pane-QmJb9ZTN.js +0 -2
  102. package/lib/_chunks/pane-QmJb9ZTN.js.map +0 -1
  103. package/lib/_chunks/pane-SK7FWNTJ.js +0 -2
  104. package/lib/_chunks/pane-SK7FWNTJ.js.map +0 -1
  105. package/lib/_chunks/pane-y4hpcKe1.js +0 -5
  106. package/lib/_chunks/pane-y4hpcKe1.js.map +0 -1
  107. package/lib/_chunks/pane-yQXBQyyI.js +0 -5
  108. package/lib/_chunks/pane-yQXBQyyI.js.map +0 -1
  109. package/lib/_chunks/structure-588eAwLd.js.map +0 -1
  110. package/lib/_chunks/structure-qJLnDJXq.js.map +0 -1
  111. package/lib/_chunks/validateAction-GUvMkXiN.js.map +0 -1
  112. 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 {formatTransaction} from './utils/mutationFormatter'
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 <name>
32
+ sanity migration run <id>
31
33
 
32
34
  # execute the migration against a dataset
33
- sanity migration run <name> --no-dry --projectId xyz --dataset staging
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 <name> --dry false --from-export=production.tar.gz --projectId xyz --dataset staging
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, migrationName: string) {
52
- return [migrationName, path.join(migrationName, 'index')].flatMap((location) =>
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
- // console.error(err)
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 createMigrationCommand: CliCommandDefinition<CreateFlags> = {
72
+ const runMigrationCommand: CliCommandDefinition<CreateFlags> = {
69
73
  name: 'run',
70
74
  group: 'migration',
71
- signature: '[NAME] [MIGRATION NAME]',
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 [migrationName] = args.argsWithoutOptions
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 (!migrationName) {
92
- throw new Error('MIGRATION NAME must be provided. `sanity migration run <name>`')
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, migrationName)
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 "${migrationName}" in current directory ${candidates
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 "${migrationName}" in current directory. Make sure that the migration file exists and exports a valid migration as its default export.\n
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
- const spinner = output.spinner(`Running migration "${migrationName}" in dry mode`).start()
167
- if (fromExport) {
168
- await runFromArchive(migration, fromExport, {
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
- const spinner = output.spinner(`Running migration "${migrationName}"`).start()
195
- await run({api: apiConfig, concurrency, onProgress: createProgress(spinner)}, migration)
196
- spinner.stop()
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
- function createProgress(spinner: ReturnType<typeof output.spinner>) {
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
- spinner.stop()
210
+ progressSpinner.stop()
203
211
  return
204
212
  }
205
213
  if (progress.done) {
206
- spinner.text = `Migration "${migrationName}" completed.
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
- spinner.stopAndPersist({symbol: chalk.green('✔')})
222
+ progressSpinner.stopAndPersist({symbol: chalk.green('✔')})
215
223
  return
216
224
  }
217
225
 
218
226
  ;[null, ...progress.currentTransactions].forEach((transaction) => {
219
- spinner.text = `Running migration "${migrationName}"
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
- ${transaction && !progress.done ? `» ${chalk.grey(formatTransaction(chalk, transaction))}` : ''}`
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 createMigrationCommand
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
- name: '${migrationName}',
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
- name: '${migrationName}',
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
- name: '${migrationName}',
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, path, context) {
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
- name: '${migrationName}',
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
- name: '${migrationName}',
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 {getStudioConfig} from '../util/getStudioConfig'
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 getStudioConfig({basePath: workDir})
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 {getStudioConfig} from '../util/getStudioConfig'
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) => !document._id.startsWith('system.')
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 getStudioConfig({basePath: workDir, configPath})
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.`)