@easyling/sanity-connector 0.0.1

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 (244) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +482 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/actions/bulkTranslate.d.ts +7 -0
  5. package/dist/actions/bulkTranslate.d.ts.map +1 -0
  6. package/dist/actions/bulkTranslate.js +543 -0
  7. package/dist/actions/bulkTranslate.js.map +1 -0
  8. package/dist/actions/manageDNTFields.d.ts +12 -0
  9. package/dist/actions/manageDNTFields.d.ts.map +1 -0
  10. package/dist/actions/manageDNTFields.js +100 -0
  11. package/dist/actions/manageDNTFields.js.map +1 -0
  12. package/dist/actions/translateDocument.d.ts +7 -0
  13. package/dist/actions/translateDocument.d.ts.map +1 -0
  14. package/dist/actions/translateDocument.js +256 -0
  15. package/dist/actions/translateDocument.js.map +1 -0
  16. package/dist/components/RadioWithDefault.d.ts +17 -0
  17. package/dist/components/RadioWithDefault.d.ts.map +1 -0
  18. package/dist/components/RadioWithDefault.js +29 -0
  19. package/dist/components/RadioWithDefault.js.map +1 -0
  20. package/dist/components/auth/AuthNavbar.d.ts +18 -0
  21. package/dist/components/auth/AuthNavbar.d.ts.map +1 -0
  22. package/dist/components/auth/AuthNavbar.js +15 -0
  23. package/dist/components/auth/AuthNavbar.js.map +1 -0
  24. package/dist/components/auth/AuthStatus.d.ts +27 -0
  25. package/dist/components/auth/AuthStatus.d.ts.map +1 -0
  26. package/dist/components/auth/AuthStatus.js +82 -0
  27. package/dist/components/auth/AuthStatus.js.map +1 -0
  28. package/dist/components/auth/AuthStatusWrapper.d.ts +15 -0
  29. package/dist/components/auth/AuthStatusWrapper.d.ts.map +1 -0
  30. package/dist/components/auth/AuthStatusWrapper.js +39 -0
  31. package/dist/components/auth/AuthStatusWrapper.js.map +1 -0
  32. package/dist/components/auth/MigrationPrompt.d.ts +20 -0
  33. package/dist/components/auth/MigrationPrompt.d.ts.map +1 -0
  34. package/dist/components/auth/MigrationPrompt.js +16 -0
  35. package/dist/components/auth/MigrationPrompt.js.map +1 -0
  36. package/dist/components/auth/MigrationPromptWrapper.d.ts +14 -0
  37. package/dist/components/auth/MigrationPromptWrapper.d.ts.map +1 -0
  38. package/dist/components/auth/MigrationPromptWrapper.js +16 -0
  39. package/dist/components/auth/MigrationPromptWrapper.js.map +1 -0
  40. package/dist/components/auth/OAuthCallback.d.ts +20 -0
  41. package/dist/components/auth/OAuthCallback.d.ts.map +1 -0
  42. package/dist/components/auth/OAuthCallback.js +12 -0
  43. package/dist/components/auth/OAuthCallback.js.map +1 -0
  44. package/dist/components/auth/index.d.ts +15 -0
  45. package/dist/components/auth/index.d.ts.map +1 -0
  46. package/dist/components/auth/index.js +12 -0
  47. package/dist/components/auth/index.js.map +1 -0
  48. package/dist/components/config/LocaleConfigTool.d.ts +17 -0
  49. package/dist/components/config/LocaleConfigTool.d.ts.map +1 -0
  50. package/dist/components/config/LocaleConfigTool.js +186 -0
  51. package/dist/components/config/LocaleConfigTool.js.map +1 -0
  52. package/dist/components/config/LocaleConfigToolWrapper.d.ts +13 -0
  53. package/dist/components/config/LocaleConfigToolWrapper.d.ts.map +1 -0
  54. package/dist/components/config/LocaleConfigToolWrapper.js +26 -0
  55. package/dist/components/config/LocaleConfigToolWrapper.js.map +1 -0
  56. package/dist/components/config/OAuthConfig.d.ts +26 -0
  57. package/dist/components/config/OAuthConfig.d.ts.map +1 -0
  58. package/dist/components/config/OAuthConfig.js +152 -0
  59. package/dist/components/config/OAuthConfig.js.map +1 -0
  60. package/dist/components/config/OAuthConfigWrapper.d.ts +13 -0
  61. package/dist/components/config/OAuthConfigWrapper.d.ts.map +1 -0
  62. package/dist/components/config/OAuthConfigWrapper.js +41 -0
  63. package/dist/components/config/OAuthConfigWrapper.js.map +1 -0
  64. package/dist/components/config/PasswordInput.d.ts +14 -0
  65. package/dist/components/config/PasswordInput.d.ts.map +1 -0
  66. package/dist/components/config/PasswordInput.js +23 -0
  67. package/dist/components/config/PasswordInput.js.map +1 -0
  68. package/dist/components/config/index.d.ts +9 -0
  69. package/dist/components/config/index.d.ts.map +1 -0
  70. package/dist/components/config/index.js +8 -0
  71. package/dist/components/config/index.js.map +1 -0
  72. package/dist/components/config/localeConfigToolDefinition.d.ts +13 -0
  73. package/dist/components/config/localeConfigToolDefinition.d.ts.map +1 -0
  74. package/dist/components/config/localeConfigToolDefinition.js +19 -0
  75. package/dist/components/config/localeConfigToolDefinition.js.map +1 -0
  76. package/dist/components/config/oauthConfigToolDefinition.d.ts +13 -0
  77. package/dist/components/config/oauthConfigToolDefinition.d.ts.map +1 -0
  78. package/dist/components/config/oauthConfigToolDefinition.js +19 -0
  79. package/dist/components/config/oauthConfigToolDefinition.js.map +1 -0
  80. package/dist/components/dialogs/ConfirmationDialog.d.ts +21 -0
  81. package/dist/components/dialogs/ConfirmationDialog.d.ts.map +1 -0
  82. package/dist/components/dialogs/ConfirmationDialog.js +28 -0
  83. package/dist/components/dialogs/ConfirmationDialog.js.map +1 -0
  84. package/dist/components/dialogs/ErrorDialog.d.ts +21 -0
  85. package/dist/components/dialogs/ErrorDialog.d.ts.map +1 -0
  86. package/dist/components/dialogs/ErrorDialog.js +28 -0
  87. package/dist/components/dialogs/ErrorDialog.js.map +1 -0
  88. package/dist/components/dialogs/LocaleSelectionDialog.d.ts +41 -0
  89. package/dist/components/dialogs/LocaleSelectionDialog.d.ts.map +1 -0
  90. package/dist/components/dialogs/LocaleSelectionDialog.js +117 -0
  91. package/dist/components/dialogs/LocaleSelectionDialog.js.map +1 -0
  92. package/dist/components/dialogs/SuccessDialog.d.ts +19 -0
  93. package/dist/components/dialogs/SuccessDialog.d.ts.map +1 -0
  94. package/dist/components/dialogs/SuccessDialog.js +37 -0
  95. package/dist/components/dialogs/SuccessDialog.js.map +1 -0
  96. package/dist/components/dialogs/index.d.ts +12 -0
  97. package/dist/components/dialogs/index.d.ts.map +1 -0
  98. package/dist/components/dialogs/index.js +8 -0
  99. package/dist/components/dialogs/index.js.map +1 -0
  100. package/dist/components/dnt/DNTFieldBadge.d.ts +16 -0
  101. package/dist/components/dnt/DNTFieldBadge.d.ts.map +1 -0
  102. package/dist/components/dnt/DNTFieldBadge.js +56 -0
  103. package/dist/components/dnt/DNTFieldBadge.js.map +1 -0
  104. package/dist/components/dnt/DNTFieldComponent.d.ts +17 -0
  105. package/dist/components/dnt/DNTFieldComponent.d.ts.map +1 -0
  106. package/dist/components/dnt/DNTFieldComponent.js +21 -0
  107. package/dist/components/dnt/DNTFieldComponent.js.map +1 -0
  108. package/dist/components/dnt/DNTFieldInput.d.ts +14 -0
  109. package/dist/components/dnt/DNTFieldInput.d.ts.map +1 -0
  110. package/dist/components/dnt/DNTFieldInput.js +76 -0
  111. package/dist/components/dnt/DNTFieldInput.js.map +1 -0
  112. package/dist/components/dnt/index.d.ts +7 -0
  113. package/dist/components/dnt/index.d.ts.map +1 -0
  114. package/dist/components/dnt/index.js +7 -0
  115. package/dist/components/dnt/index.js.map +1 -0
  116. package/dist/config/index.d.ts +6 -0
  117. package/dist/config/index.d.ts.map +1 -0
  118. package/dist/config/index.js +6 -0
  119. package/dist/config/index.js.map +1 -0
  120. package/dist/config/pluginConfig.d.ts +163 -0
  121. package/dist/config/pluginConfig.d.ts.map +1 -0
  122. package/dist/config/pluginConfig.js +548 -0
  123. package/dist/config/pluginConfig.js.map +1 -0
  124. package/dist/index.d.ts +12 -0
  125. package/dist/index.d.ts.map +1 -0
  126. package/dist/index.js +12 -0
  127. package/dist/index.js.map +1 -0
  128. package/dist/plugin.d.ts +3 -0
  129. package/dist/plugin.d.ts.map +1 -0
  130. package/dist/plugin.js +137 -0
  131. package/dist/plugin.js.map +1 -0
  132. package/dist/services/authStateManager.d.ts +94 -0
  133. package/dist/services/authStateManager.d.ts.map +1 -0
  134. package/dist/services/authStateManager.js +208 -0
  135. package/dist/services/authStateManager.js.map +1 -0
  136. package/dist/services/contentExtractor.d.ts +95 -0
  137. package/dist/services/contentExtractor.d.ts.map +1 -0
  138. package/dist/services/contentExtractor.js +516 -0
  139. package/dist/services/contentExtractor.js.map +1 -0
  140. package/dist/services/dialogService.d.ts +96 -0
  141. package/dist/services/dialogService.d.ts.map +1 -0
  142. package/dist/services/dialogService.js +244 -0
  143. package/dist/services/dialogService.js.map +1 -0
  144. package/dist/services/dntServiceManager.d.ts +44 -0
  145. package/dist/services/dntServiceManager.d.ts.map +1 -0
  146. package/dist/services/dntServiceManager.js +74 -0
  147. package/dist/services/dntServiceManager.js.map +1 -0
  148. package/dist/services/dntStorageAdapter.d.ts +73 -0
  149. package/dist/services/dntStorageAdapter.d.ts.map +1 -0
  150. package/dist/services/dntStorageAdapter.js +192 -0
  151. package/dist/services/dntStorageAdapter.js.map +1 -0
  152. package/dist/services/documentCreationService.d.ts +139 -0
  153. package/dist/services/documentCreationService.d.ts.map +1 -0
  154. package/dist/services/documentCreationService.js +938 -0
  155. package/dist/services/documentCreationService.js.map +1 -0
  156. package/dist/services/localeService.d.ts +160 -0
  157. package/dist/services/localeService.d.ts.map +1 -0
  158. package/dist/services/localeService.js +300 -0
  159. package/dist/services/localeService.js.map +1 -0
  160. package/dist/services/localeStorageAdapter.d.ts +42 -0
  161. package/dist/services/localeStorageAdapter.d.ts.map +1 -0
  162. package/dist/services/localeStorageAdapter.js +107 -0
  163. package/dist/services/localeStorageAdapter.js.map +1 -0
  164. package/dist/services/oauthConfigStorage.d.ts +46 -0
  165. package/dist/services/oauthConfigStorage.d.ts.map +1 -0
  166. package/dist/services/oauthConfigStorage.js +122 -0
  167. package/dist/services/oauthConfigStorage.js.map +1 -0
  168. package/dist/services/oauthService.d.ts +48 -0
  169. package/dist/services/oauthService.d.ts.map +1 -0
  170. package/dist/services/oauthService.js +71 -0
  171. package/dist/services/oauthService.js.map +1 -0
  172. package/dist/services/oauthServiceManager.d.ts +189 -0
  173. package/dist/services/oauthServiceManager.d.ts.map +1 -0
  174. package/dist/services/oauthServiceManager.js +380 -0
  175. package/dist/services/oauthServiceManager.js.map +1 -0
  176. package/dist/services/tokenStorage.d.ts +54 -0
  177. package/dist/services/tokenStorage.d.ts.map +1 -0
  178. package/dist/services/tokenStorage.js +140 -0
  179. package/dist/services/tokenStorage.js.map +1 -0
  180. package/dist/services/translationService.d.ts +374 -0
  181. package/dist/services/translationService.d.ts.map +1 -0
  182. package/dist/services/translationService.js +687 -0
  183. package/dist/services/translationService.js.map +1 -0
  184. package/dist/services/unifiedConfigStorage.d.ts +124 -0
  185. package/dist/services/unifiedConfigStorage.d.ts.map +1 -0
  186. package/dist/services/unifiedConfigStorage.js +304 -0
  187. package/dist/services/unifiedConfigStorage.js.map +1 -0
  188. package/dist/test-utils.d.ts +9 -0
  189. package/dist/test-utils.d.ts.map +1 -0
  190. package/dist/test-utils.js +13 -0
  191. package/dist/test-utils.js.map +1 -0
  192. package/dist/types/dialog.d.ts +107 -0
  193. package/dist/types/dialog.d.ts.map +1 -0
  194. package/dist/types/dialog.js +6 -0
  195. package/dist/types/dialog.js.map +1 -0
  196. package/dist/types/dnt.d.ts +84 -0
  197. package/dist/types/dnt.d.ts.map +1 -0
  198. package/dist/types/dnt.js +5 -0
  199. package/dist/types/dnt.js.map +1 -0
  200. package/dist/types/index.d.ts +12 -0
  201. package/dist/types/index.d.ts.map +1 -0
  202. package/dist/types/index.js +6 -0
  203. package/dist/types/index.js.map +1 -0
  204. package/dist/types/locale.d.ts +116 -0
  205. package/dist/types/locale.d.ts.map +1 -0
  206. package/dist/types/locale.js +189 -0
  207. package/dist/types/locale.js.map +1 -0
  208. package/dist/types/oauth.d.ts +90 -0
  209. package/dist/types/oauth.d.ts.map +1 -0
  210. package/dist/types/oauth.js +62 -0
  211. package/dist/types/oauth.js.map +1 -0
  212. package/dist/types/pluginConfig.d.ts +45 -0
  213. package/dist/types/pluginConfig.d.ts.map +1 -0
  214. package/dist/types/pluginConfig.js +6 -0
  215. package/dist/types/pluginConfig.js.map +1 -0
  216. package/dist/types/translation.d.ts +122 -0
  217. package/dist/types/translation.d.ts.map +1 -0
  218. package/dist/types/translation.js +6 -0
  219. package/dist/types/translation.js.map +1 -0
  220. package/dist/utils/htmlFormatter.d.ts +66 -0
  221. package/dist/utils/htmlFormatter.d.ts.map +1 -0
  222. package/dist/utils/htmlFormatter.js +191 -0
  223. package/dist/utils/htmlFormatter.js.map +1 -0
  224. package/dist/utils/index.d.ts +15 -0
  225. package/dist/utils/index.d.ts.map +1 -0
  226. package/dist/utils/index.js +16 -0
  227. package/dist/utils/index.js.map +1 -0
  228. package/dist/utils/logger.d.ts +105 -0
  229. package/dist/utils/logger.d.ts.map +1 -0
  230. package/dist/utils/logger.js +229 -0
  231. package/dist/utils/logger.js.map +1 -0
  232. package/dist/utils/oauthErrorFeedback.d.ts +76 -0
  233. package/dist/utils/oauthErrorFeedback.d.ts.map +1 -0
  234. package/dist/utils/oauthErrorFeedback.js +134 -0
  235. package/dist/utils/oauthErrorFeedback.js.map +1 -0
  236. package/dist/utils/oauthLogger.d.ts +176 -0
  237. package/dist/utils/oauthLogger.d.ts.map +1 -0
  238. package/dist/utils/oauthLogger.js +282 -0
  239. package/dist/utils/oauthLogger.js.map +1 -0
  240. package/dist/utils/validator.d.ts +67 -0
  241. package/dist/utils/validator.d.ts.map +1 -0
  242. package/dist/utils/validator.js +390 -0
  243. package/dist/utils/validator.js.map +1 -0
  244. package/package.json +80 -0
@@ -0,0 +1,938 @@
1
+ export class DocumentCreationService {
2
+ constructor(client, defaultCreationMode) {
3
+ this.defaultCreationMode = 'draft';
4
+ this.client = client;
5
+ if (defaultCreationMode) {
6
+ this.defaultCreationMode = defaultCreationMode;
7
+ }
8
+ }
9
+ /**
10
+ * Set the default document creation mode
11
+ * @param mode - The creation mode to use by default ('draft' or 'published')
12
+ */
13
+ setDefaultCreationMode(mode) {
14
+ this.defaultCreationMode = mode;
15
+ }
16
+ /**
17
+ * Create a new Sanity document from translation response
18
+ * Requirements: 1.1, 1.2, 2.1, 3.2, 3.4
19
+ */
20
+ async createDocumentFromTranslation(originalDocument, translationResponse, options = {}) {
21
+ try {
22
+ // Enhanced input validation
23
+ const inputValidation = this.validateInputsForDocumentCreation(originalDocument, translationResponse, options);
24
+ if (!inputValidation.isValid) {
25
+ return {
26
+ success: false,
27
+ error: `Input validation failed: ${inputValidation.errors.join(', ')}`
28
+ };
29
+ }
30
+ // Get translated content (support both new array format and legacy formats)
31
+ const translatedContent = translationResponse.translatedDocuments?.[0] ||
32
+ translationResponse.translatedContent;
33
+ if (!translationResponse.success || !translatedContent) {
34
+ return {
35
+ success: false,
36
+ error: 'Translation response does not contain valid translated content'
37
+ };
38
+ }
39
+ // Validate translation response content
40
+ const responseValidation = this.validateTranslationResponseForCreation(translationResponse);
41
+ if (!responseValidation.isValid) {
42
+ return {
43
+ success: false,
44
+ error: `Translation response validation failed: ${responseValidation.errors.join(', ')}`
45
+ };
46
+ }
47
+ // Merge translated content back into original document structure
48
+ const newDocument = await this.mergeTranslatedContent(originalDocument, translatedContent, options);
49
+ // Validate the new document structure
50
+ const validationResult = this.validateDocumentStructure(newDocument);
51
+ if (!validationResult.isValid) {
52
+ return {
53
+ success: false,
54
+ error: `Document validation failed: ${validationResult.errors.join(', ')}`
55
+ };
56
+ }
57
+ // Additional business logic validation
58
+ const businessValidation = this.validateBusinessRules(newDocument, originalDocument);
59
+ if (!businessValidation.isValid) {
60
+ return {
61
+ success: false,
62
+ error: `Business rule validation failed: ${businessValidation.errors.join(', ')}`
63
+ };
64
+ }
65
+ // Determine effective creation mode (option override or default)
66
+ const effectiveCreationMode = options.creationMode || this.defaultCreationMode;
67
+ // Actually create the document in Sanity
68
+ try {
69
+ let createdDocument;
70
+ if (effectiveCreationMode === 'draft') {
71
+ // Create as draft document by prefixing ID with 'drafts.'
72
+ const draftId = newDocument._id.startsWith('drafts.')
73
+ ? newDocument._id
74
+ : `drafts.${newDocument._id}`;
75
+ console.log('Creating draft document in Sanity:', draftId);
76
+ createdDocument = await this.client.create({
77
+ ...newDocument,
78
+ _id: draftId
79
+ });
80
+ console.log('Draft document created successfully:', createdDocument._id);
81
+ }
82
+ else {
83
+ // Create as published document (no prefix)
84
+ // Strip 'drafts.' prefix if present to ensure it's a published document
85
+ const publishedId = newDocument._id.startsWith('drafts.')
86
+ ? newDocument._id.replace(/^drafts\./, '')
87
+ : newDocument._id;
88
+ console.log('Creating published document in Sanity:', publishedId);
89
+ createdDocument = await this.client.create({
90
+ ...newDocument,
91
+ _id: publishedId
92
+ });
93
+ console.log('Published document created successfully:', createdDocument._id);
94
+ }
95
+ return {
96
+ success: true,
97
+ documentId: createdDocument._id,
98
+ document: createdDocument
99
+ };
100
+ }
101
+ catch (sanityError) {
102
+ console.error('Failed to create document in Sanity:', sanityError);
103
+ return {
104
+ success: false,
105
+ error: `Failed to save document to Sanity: ${sanityError instanceof Error ? sanityError.message : 'Unknown Sanity error'}`
106
+ };
107
+ }
108
+ }
109
+ catch (error) {
110
+ // Enhanced error handling with more context
111
+ const errorMessage = this.formatErrorMessage(error, originalDocument._id);
112
+ return {
113
+ success: false,
114
+ error: errorMessage
115
+ };
116
+ }
117
+ }
118
+ /**
119
+ * Validate inputs for document creation
120
+ * Requirements: 4.3, 4.4
121
+ */
122
+ validateInputsForDocumentCreation(originalDocument, translationResponse, options) {
123
+ const errors = [];
124
+ // Validate original document
125
+ if (!originalDocument) {
126
+ errors.push('Original document is required');
127
+ return { isValid: false, errors };
128
+ }
129
+ if (!originalDocument._id) {
130
+ errors.push('Original document must have an _id');
131
+ }
132
+ if (!originalDocument._type) {
133
+ errors.push('Original document must have a _type');
134
+ }
135
+ // Validate translation response
136
+ if (!translationResponse) {
137
+ errors.push('Translation response is required');
138
+ return { isValid: false, errors };
139
+ }
140
+ // Validate options
141
+ if (options.targetLanguage && typeof options.targetLanguage !== 'string') {
142
+ errors.push('Target language must be a string');
143
+ }
144
+ if (options.sourceLanguage && typeof options.sourceLanguage !== 'string') {
145
+ errors.push('Source language must be a string');
146
+ }
147
+ if (options.createAsVariant !== undefined && typeof options.createAsVariant !== 'boolean') {
148
+ errors.push('createAsVariant must be a boolean');
149
+ }
150
+ if (options.parentDocumentId && typeof options.parentDocumentId !== 'string') {
151
+ errors.push('parentDocumentId must be a string');
152
+ }
153
+ return {
154
+ isValid: errors.length === 0,
155
+ errors
156
+ };
157
+ }
158
+ /**
159
+ * Validate business rules for document creation
160
+ * Requirements: 4.3, 4.4
161
+ */
162
+ validateBusinessRules(newDocument, originalDocument) {
163
+ const errors = [];
164
+ // Ensure new document has different ID from original
165
+ if (newDocument._id === originalDocument._id) {
166
+ errors.push('New document ID must be different from original document ID');
167
+ }
168
+ // Ensure new document has same type as original
169
+ if (newDocument._type !== originalDocument._type) {
170
+ errors.push(`New document type (${newDocument._type}) must match original document type (${originalDocument._type})`);
171
+ }
172
+ // Validate that essential content exists
173
+ const hasContent = this.documentHasContent(newDocument);
174
+ if (!hasContent) {
175
+ errors.push('New document must contain some translatable content');
176
+ }
177
+ // Validate slug uniqueness if present
178
+ if (newDocument.slug && originalDocument.slug) {
179
+ const newSlug = typeof newDocument.slug === 'object' ? newDocument.slug.current : newDocument.slug;
180
+ const originalSlug = typeof originalDocument.slug === 'object' ? originalDocument.slug.current : originalDocument.slug;
181
+ if (newSlug === originalSlug) {
182
+ errors.push('New document slug should be different from original to avoid conflicts');
183
+ }
184
+ }
185
+ return {
186
+ isValid: errors.length === 0,
187
+ errors
188
+ };
189
+ }
190
+ /**
191
+ * Check if document has meaningful content
192
+ */
193
+ documentHasContent(document) {
194
+ // Check for title
195
+ if (document.title && typeof document.title === 'string' && document.title.trim().length > 0) {
196
+ return true;
197
+ }
198
+ // Check for body content
199
+ const contentFields = ['body', 'content', 'text', 'description'];
200
+ for (const field of contentFields) {
201
+ if (document[field]) {
202
+ if (typeof document[field] === 'string' && document[field].trim().length > 0) {
203
+ return true;
204
+ }
205
+ if (Array.isArray(document[field]) && document[field].length > 0) {
206
+ return true;
207
+ }
208
+ }
209
+ }
210
+ return false;
211
+ }
212
+ /**
213
+ * Format error messages with additional context
214
+ * Requirements: 4.3, 4.4
215
+ */
216
+ formatErrorMessage(error, documentId) {
217
+ const baseMessage = error instanceof Error ? error.message : 'Unknown error occurred during document creation';
218
+ const contextInfo = documentId ? ` (Document ID: ${documentId})` : '';
219
+ // Add specific error handling for common issues
220
+ if (error instanceof Error) {
221
+ if (error.message.includes('validation')) {
222
+ return `Document validation error${contextInfo}: ${error.message}`;
223
+ }
224
+ if (error.message.includes('network') || error.message.includes('fetch')) {
225
+ return `Network error during document creation${contextInfo}: ${error.message}`;
226
+ }
227
+ if (error.message.includes('permission') || error.message.includes('unauthorized')) {
228
+ return `Permission error during document creation${contextInfo}: ${error.message}`;
229
+ }
230
+ }
231
+ return `Document creation error${contextInfo}: ${baseMessage}`;
232
+ }
233
+ /**
234
+ * Create multiple documents from bulk translation responses
235
+ * Requirements: 2.1, 3.2, 3.4
236
+ */
237
+ async createBulkDocumentsFromTranslations(originalDocuments, translationResponses, options = {}) {
238
+ const result = {
239
+ totalDocuments: originalDocuments.length,
240
+ successfulCreations: 0,
241
+ failedCreations: 0,
242
+ results: [],
243
+ errors: [],
244
+ rollbackInfo: {
245
+ canRollback: true,
246
+ createdDocumentIds: [],
247
+ rollbackInstructions: []
248
+ }
249
+ };
250
+ // Pre-validate all translation responses before creating any documents
251
+ const validationErrors = this.validateBulkTranslationResponses(originalDocuments, translationResponses);
252
+ if (validationErrors.length > 0) {
253
+ result.errors.push(...validationErrors);
254
+ result.failedCreations = originalDocuments.length;
255
+ result.rollbackInfo.canRollback = false;
256
+ result.rollbackInfo.rollbackInstructions.push('No documents were created due to validation failures');
257
+ // Add individual failure results
258
+ for (let i = 0; i < originalDocuments.length; i++) {
259
+ result.results.push({
260
+ success: false,
261
+ error: 'Bulk validation failed - no documents created'
262
+ });
263
+ }
264
+ return result;
265
+ }
266
+ // Track successful creations for potential rollback
267
+ const createdDocuments = [];
268
+ for (let i = 0; i < originalDocuments.length; i++) {
269
+ const originalDocument = originalDocuments[i];
270
+ const translationResponse = translationResponses[i];
271
+ if (!translationResponse) {
272
+ const error = `No translation response found for document ${originalDocument._id}`;
273
+ result.results.push({
274
+ success: false,
275
+ error
276
+ });
277
+ result.errors.push(error);
278
+ result.failedCreations++;
279
+ continue;
280
+ }
281
+ try {
282
+ // Additional validation before creation
283
+ const preCreationValidation = this.validateTranslationResponseForCreation(translationResponse);
284
+ if (!preCreationValidation.isValid) {
285
+ const error = `Pre-creation validation failed for ${originalDocument._id}: ${preCreationValidation.errors.join(', ')}`;
286
+ result.results.push({
287
+ success: false,
288
+ error
289
+ });
290
+ result.errors.push(error);
291
+ result.failedCreations++;
292
+ continue;
293
+ }
294
+ // Get all translated documents (support both new array format and legacy single document format)
295
+ const translatedDocuments = translationResponse.translatedDocuments ||
296
+ (translationResponse.translatedContent ? [translationResponse.translatedContent] : []);
297
+ if (translatedDocuments.length === 0) {
298
+ const error = `No translated documents found in response for ${originalDocument._id}`;
299
+ result.results.push({
300
+ success: false,
301
+ error
302
+ });
303
+ result.errors.push(error);
304
+ result.failedCreations++;
305
+ continue;
306
+ }
307
+ // Create a document for each translated document in the response
308
+ for (let j = 0; j < translatedDocuments.length; j++) {
309
+ const translatedContent = translatedDocuments[j];
310
+ // Extract locale from the translated content
311
+ const localeField = translatedContent.fields?.locale;
312
+ const localeValue = localeField && typeof localeField === 'object' && 'value' in localeField
313
+ ? localeField.value
314
+ : localeField;
315
+ const targetLocale = (localeValue ||
316
+ translatedContent.locale ||
317
+ options.targetLanguage ||
318
+ 'translated');
319
+ console.log(`Creating document for ${originalDocument._id} with locale: ${targetLocale}`);
320
+ // Create a single-document translation response for this document
321
+ const singleDocumentResponse = {
322
+ ...translationResponse,
323
+ translatedDocuments: [translatedContent],
324
+ translatedContent: translatedContent
325
+ };
326
+ const creationResult = await this.createDocumentFromTranslation(originalDocument, singleDocumentResponse, {
327
+ ...options,
328
+ targetLanguage: targetLocale
329
+ });
330
+ result.results.push(creationResult);
331
+ if (creationResult.success && creationResult.documentId) {
332
+ result.successfulCreations++;
333
+ createdDocuments.push({
334
+ documentId: creationResult.documentId,
335
+ originalId: originalDocument._id
336
+ });
337
+ result.rollbackInfo.createdDocumentIds.push(creationResult.documentId);
338
+ }
339
+ else {
340
+ result.failedCreations++;
341
+ if (creationResult.error) {
342
+ result.errors.push(creationResult.error);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ catch (error) {
348
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
349
+ result.results.push({
350
+ success: false,
351
+ error: errorMessage
352
+ });
353
+ result.errors.push(errorMessage);
354
+ result.failedCreations++;
355
+ }
356
+ }
357
+ // Generate rollback instructions
358
+ if (result.successfulCreations > 0) {
359
+ result.rollbackInfo.rollbackInstructions = [
360
+ `${result.successfulCreations} documents were successfully created.`,
361
+ 'To rollback these changes, you would need to delete the following document IDs:',
362
+ ...result.rollbackInfo.createdDocumentIds.map(id => `- ${id}`),
363
+ 'Note: This plugin does not automatically delete documents for safety reasons.',
364
+ 'Manual deletion through Sanity Studio or API calls would be required.'
365
+ ];
366
+ }
367
+ else {
368
+ result.rollbackInfo.rollbackInstructions = ['No documents were created, no rollback needed.'];
369
+ }
370
+ return result;
371
+ }
372
+ /**
373
+ * Validate bulk translation responses before document creation
374
+ * Requirements: 4.3, 4.4
375
+ */
376
+ validateBulkTranslationResponses(originalDocuments, translationResponses) {
377
+ const errors = [];
378
+ // Check array lengths match
379
+ if (originalDocuments.length !== translationResponses.length) {
380
+ errors.push(`Mismatch between original documents (${originalDocuments.length}) and translation responses (${translationResponses.length})`);
381
+ return errors; // Critical error, return immediately
382
+ }
383
+ // Validate each pair
384
+ for (let i = 0; i < originalDocuments.length; i++) {
385
+ const originalDoc = originalDocuments[i];
386
+ const response = translationResponses[i];
387
+ if (!originalDoc) {
388
+ errors.push(`Original document at index ${i} is null or undefined`);
389
+ continue;
390
+ }
391
+ if (!originalDoc._id) {
392
+ errors.push(`Original document at index ${i} missing _id`);
393
+ continue;
394
+ }
395
+ if (!originalDoc._type) {
396
+ errors.push(`Original document at index ${i} missing _type`);
397
+ continue;
398
+ }
399
+ if (!response) {
400
+ errors.push(`Translation response at index ${i} is null or undefined`);
401
+ continue;
402
+ }
403
+ if (!response.success) {
404
+ // This is not necessarily an error for bulk validation, just skip this document
405
+ continue;
406
+ }
407
+ // Check for translated content (support both new array format and legacy formats)
408
+ const translatedDocuments = response.translatedDocuments ||
409
+ (response.translatedContent ? [response.translatedContent] : []);
410
+ if (translatedDocuments.length === 0) {
411
+ errors.push(`Translation response at index ${i} missing translatedDocuments/translatedContent despite success=true`);
412
+ continue;
413
+ }
414
+ }
415
+ return errors;
416
+ }
417
+ /**
418
+ * Validate translation response before document creation
419
+ * Requirements: 4.3, 4.4
420
+ */
421
+ validateTranslationResponseForCreation(response) {
422
+ const errors = [];
423
+ if (!response.success) {
424
+ errors.push('Translation response indicates failure');
425
+ return { isValid: false, errors };
426
+ }
427
+ // Get translated content (support both new array format and legacy formats)
428
+ const translatedDocuments = response.translatedDocuments ||
429
+ (response.translatedContent ? [response.translatedContent] : []);
430
+ if (translatedDocuments.length === 0) {
431
+ errors.push('Translation response missing translatedDocuments/translatedContent');
432
+ return { isValid: false, errors };
433
+ }
434
+ // Validate each translated document structure
435
+ for (let i = 0; i < translatedDocuments.length; i++) {
436
+ const content = translatedDocuments[i];
437
+ if (typeof content !== 'object' || content === null) {
438
+ errors.push(`Translated document at index ${i} must be an object`);
439
+ continue;
440
+ }
441
+ // Check for required fields
442
+ if (!content.body && !content.title && !content.fields) {
443
+ errors.push(`Translated document at index ${i} must contain at least one of: body, title, or fields`);
444
+ }
445
+ // Validate body content if present
446
+ if (content.body !== undefined) {
447
+ if (typeof content.body !== 'string') {
448
+ errors.push(`Translated document at index ${i} body must be a string`);
449
+ }
450
+ else if (content.body.trim().length === 0) {
451
+ errors.push(`Translated document at index ${i} body cannot be empty`);
452
+ }
453
+ }
454
+ // Validate title if present
455
+ if (content.title !== undefined) {
456
+ if (typeof content.title !== 'string') {
457
+ errors.push(`Translated document at index ${i} title must be a string`);
458
+ }
459
+ else if (content.title.trim().length === 0) {
460
+ errors.push(`Translated document at index ${i} title cannot be empty`);
461
+ }
462
+ }
463
+ // Validate slug if present
464
+ if (content.slug !== undefined) {
465
+ if (typeof content.slug !== 'string') {
466
+ errors.push(`Translated document at index ${i} slug must be a string`);
467
+ }
468
+ else if (content.slug.trim().length === 0) {
469
+ errors.push(`Translated document at index ${i} slug cannot be empty`);
470
+ }
471
+ else if (!/^[a-z0-9-_]+$/.test(content.slug)) {
472
+ errors.push(`Translated document at index ${i} slug must contain only lowercase letters, numbers, hyphens, and underscores`);
473
+ }
474
+ }
475
+ // Validate fields if present
476
+ if (content.fields !== undefined) {
477
+ if (typeof content.fields !== 'object' || content.fields === null || Array.isArray(content.fields)) {
478
+ errors.push(`Translated document at index ${i} fields must be an object`);
479
+ }
480
+ }
481
+ }
482
+ return {
483
+ isValid: errors.length === 0,
484
+ errors
485
+ };
486
+ }
487
+ /**
488
+ * Create rollback plan for failed bulk operations
489
+ * Requirements: 4.3, 4.4
490
+ */
491
+ createRollbackPlan(createdDocumentIds) {
492
+ if (createdDocumentIds.length === 0) {
493
+ return {
494
+ canRollback: true,
495
+ createdDocumentIds: [],
496
+ rollbackInstructions: ['No documents were created, no rollback needed.']
497
+ };
498
+ }
499
+ return {
500
+ canRollback: true,
501
+ createdDocumentIds: [...createdDocumentIds],
502
+ rollbackInstructions: [
503
+ `Rollback plan for ${createdDocumentIds.length} created documents:`,
504
+ '',
505
+ '1. Manual deletion required (for safety):',
506
+ ...createdDocumentIds.map(id => ` - Delete document: ${id}`),
507
+ '',
508
+ '2. Alternative: Use Sanity CLI or API:',
509
+ ' sanity documents delete <document-id>',
510
+ '',
511
+ '3. Or via Sanity Client API:',
512
+ ' client.delete(documentId)',
513
+ '',
514
+ 'WARNING: This plugin does not provide automatic rollback to prevent',
515
+ 'accidental data loss. Please review each document before deletion.'
516
+ ]
517
+ };
518
+ }
519
+ /**
520
+ * Merge translated content back into original document structure
521
+ * Requirements: 1.2, 3.2, 3.4
522
+ */
523
+ async mergeTranslatedContent(originalDocument, translatedContent, options) {
524
+ // Extract the target locale early for document ID generation
525
+ // Priority: options.targetLanguage > translatedContent.locale > fields.locale
526
+ let targetLang = options.targetLanguage;
527
+ if (!targetLang) {
528
+ if (translatedContent.locale) {
529
+ targetLang = translatedContent.locale;
530
+ }
531
+ else if (translatedContent.fields?.locale) {
532
+ const localeField = translatedContent.fields.locale;
533
+ if (localeField && typeof localeField === 'object' && 'value' in localeField) {
534
+ targetLang = localeField.value;
535
+ }
536
+ else if (typeof localeField === 'string') {
537
+ targetLang = localeField;
538
+ }
539
+ }
540
+ }
541
+ // Create a new document based on the original
542
+ const newDocument = {
543
+ ...originalDocument,
544
+ _id: this.generateNewDocumentId(originalDocument._id, targetLang),
545
+ // Remove system fields - they will be set by Sanity
546
+ _rev: undefined,
547
+ _createdAt: undefined,
548
+ _updatedAt: undefined,
549
+ };
550
+ // Update title if provided
551
+ if (translatedContent.title && translatedContent.title.trim()) {
552
+ newDocument.title = translatedContent.title;
553
+ }
554
+ // Update slug if provided - ensure it's in the correct format
555
+ if (translatedContent.slug && translatedContent.slug.trim()) {
556
+ // Check if original document has slug as an object with _type
557
+ if (typeof originalDocument.slug === 'object' && originalDocument.slug !== null && '_type' in originalDocument.slug) {
558
+ newDocument.slug = {
559
+ _type: 'slug',
560
+ current: translatedContent.slug
561
+ };
562
+ }
563
+ else {
564
+ // If original was a string or didn't exist, just use the string
565
+ newDocument.slug = translatedContent.slug;
566
+ }
567
+ }
568
+ // Convert HTML body back to portable text and merge
569
+ if (translatedContent.body && translatedContent.body.trim()) {
570
+ const portableTextContent = this.convertHtmlToPortableText(translatedContent.body);
571
+ // Find the main content field and update it
572
+ const contentFields = ['body', 'content', 'text', 'description'];
573
+ for (const field of contentFields) {
574
+ if (originalDocument[field]) {
575
+ newDocument[field] = portableTextContent;
576
+ break;
577
+ }
578
+ }
579
+ // If no content field found, add to body
580
+ if (!contentFields.some(field => originalDocument[field])) {
581
+ newDocument.body = portableTextContent;
582
+ }
583
+ }
584
+ // Merge other translated fields from the fields object
585
+ if (translatedContent.fields && typeof translatedContent.fields === 'object') {
586
+ for (const [fieldName, fieldValue] of Object.entries(translatedContent.fields)) {
587
+ // Skip title and slug as they're handled separately above
588
+ if (fieldName === 'title' || fieldName === 'slug' || fieldValue === undefined) {
589
+ continue;
590
+ }
591
+ // Handle the field value based on the original document's structure
592
+ const processedValue = this.processTranslatedField(fieldName, fieldValue, originalDocument[fieldName]);
593
+ if (processedValue !== undefined) {
594
+ newDocument[fieldName] = processedValue;
595
+ }
596
+ }
597
+ }
598
+ // Ensure locale field is set correctly with the target language
599
+ if (targetLang) {
600
+ // Check if original document has locale field with {dnt, value} structure
601
+ if (originalDocument.locale && typeof originalDocument.locale === 'object' && 'value' in originalDocument.locale) {
602
+ newDocument.locale = {
603
+ ...originalDocument.locale,
604
+ value: targetLang
605
+ };
606
+ }
607
+ else if (newDocument.locale && typeof newDocument.locale === 'object' && 'value' in newDocument.locale) {
608
+ // If locale field exists in newDocument but value is empty/wrong, fix it
609
+ newDocument.locale = {
610
+ ...newDocument.locale,
611
+ value: targetLang
612
+ };
613
+ }
614
+ else if (originalDocument.locale !== undefined) {
615
+ // If locale exists but is a plain string
616
+ newDocument.locale = targetLang;
617
+ }
618
+ }
619
+ // Clean up null fields that aren't defined in the original document's schema
620
+ // Remove fields that are explicitly null or undefined to avoid schema validation errors
621
+ const fieldsToClean = Object.keys(newDocument);
622
+ for (const field of fieldsToClean) {
623
+ if (newDocument[field] === null) {
624
+ delete newDocument[field];
625
+ }
626
+ }
627
+ // Add reference to parent document if creating as variant
628
+ if (options.createAsVariant && options.parentDocumentId) {
629
+ newDocument.parentDocument = {
630
+ _type: 'reference',
631
+ _ref: options.parentDocumentId
632
+ };
633
+ }
634
+ return newDocument;
635
+ }
636
+ /**
637
+ * Process a translated field value and convert it to the appropriate format
638
+ * based on the original document's structure
639
+ */
640
+ processTranslatedField(fieldName, translatedValue, originalValue) {
641
+ // If translatedValue is null or undefined, return it as-is
642
+ if (translatedValue === null || translatedValue === undefined) {
643
+ return translatedValue;
644
+ }
645
+ // Extract the actual value from TranslatableField objects
646
+ let actualValue = translatedValue;
647
+ let isDNT = false;
648
+ if (translatedValue && typeof translatedValue === 'object' && 'value' in translatedValue) {
649
+ // This is a TranslatableField object - extract the value
650
+ actualValue = translatedValue.value;
651
+ isDNT = translatedValue.dnt || false;
652
+ }
653
+ // If the field is marked as DNT, use the original value instead
654
+ if (isDNT && originalValue !== undefined) {
655
+ return originalValue;
656
+ }
657
+ // Check if the original value has a specific structure that we need to preserve
658
+ if (originalValue && typeof originalValue === 'object' && originalValue !== null) {
659
+ // Handle fields with {dnt, value} structure
660
+ if ('dnt' in originalValue && 'value' in originalValue) {
661
+ // IMPORTANT: Only use actualValue if it's not empty/null/undefined
662
+ // If actualValue is empty but translatedValue had a nested value, try to extract it
663
+ let finalValue = actualValue;
664
+ // Handle cases where translatedValue itself has a nested value structure
665
+ if (translatedValue && typeof translatedValue === 'object' && 'value' in translatedValue) {
666
+ finalValue = translatedValue.value;
667
+ }
668
+ // If finalValue is still empty/null/undefined, don't replace the original
669
+ if (finalValue === null || finalValue === undefined || finalValue === '') {
670
+ // Check if we should keep the original value
671
+ if (originalValue.value !== null && originalValue.value !== undefined && originalValue.value !== '') {
672
+ return originalValue;
673
+ }
674
+ }
675
+ return {
676
+ dnt: isDNT,
677
+ value: finalValue
678
+ };
679
+ }
680
+ // Handle slug objects
681
+ if ('_type' in originalValue && originalValue._type === 'slug') {
682
+ return {
683
+ _type: 'slug',
684
+ current: typeof actualValue === 'string' ? actualValue : String(actualValue)
685
+ };
686
+ }
687
+ // Handle date/datetime objects
688
+ if ('_type' in originalValue && (originalValue._type === 'date' || originalValue._type === 'datetime')) {
689
+ return {
690
+ _type: originalValue._type,
691
+ value: actualValue
692
+ };
693
+ }
694
+ // Handle reference objects
695
+ if ('_type' in originalValue && originalValue._type === 'reference') {
696
+ return originalValue; // Keep references unchanged
697
+ }
698
+ }
699
+ // If the actual value is an HTML string, convert it to portable text
700
+ if (typeof actualValue === 'string' && this.isHtmlContent(actualValue)) {
701
+ return this.convertHtmlToPortableText(actualValue);
702
+ }
703
+ // Return the actual value as-is
704
+ return actualValue;
705
+ }
706
+ /**
707
+ * Convert HTML content back to Sanity portable text format
708
+ * Requirements: 3.2, 3.4
709
+ */
710
+ convertHtmlToPortableText(htmlContent) {
711
+ // This is a simplified HTML to portable text conversion
712
+ // In a real implementation, you might want to use a proper HTML parser
713
+ if (!htmlContent || !htmlContent.trim()) {
714
+ return [];
715
+ }
716
+ const blocks = [];
717
+ // Use a more precise regex to match complete HTML tags and their content
718
+ const htmlTagRegex = /<(h[1-6]|p|div|blockquote|li)([^>]*)>(.*?)<\/\1>/gi;
719
+ let match;
720
+ let lastIndex = 0;
721
+ while ((match = htmlTagRegex.exec(htmlContent)) !== null) {
722
+ const [fullMatch, tagName, attributes, content] = match;
723
+ // Skip if content is empty or just whitespace
724
+ if (!content.trim()) {
725
+ continue;
726
+ }
727
+ const block = {
728
+ _type: 'block',
729
+ _key: this.generateBlockKey(),
730
+ style: this.getBlockStyle(tagName.toLowerCase()),
731
+ children: this.parseInlineContent(content),
732
+ markDefs: []
733
+ };
734
+ // Handle list items
735
+ if (tagName.toLowerCase() === 'li') {
736
+ block.listItem = 'bullet'; // Default to bullet, could be enhanced
737
+ }
738
+ blocks.push(block);
739
+ lastIndex = match.index + fullMatch.length;
740
+ }
741
+ // Handle any remaining content that wasn't matched by tags
742
+ const remainingContent = htmlContent.substring(lastIndex).trim();
743
+ if (remainingContent) {
744
+ // Remove any remaining HTML tags and create a block
745
+ const cleanContent = remainingContent.replace(/<[^>]+>/g, '').trim();
746
+ if (cleanContent) {
747
+ blocks.push({
748
+ _type: 'block',
749
+ _key: this.generateBlockKey(),
750
+ style: 'normal',
751
+ children: this.parseInlineContent(cleanContent),
752
+ markDefs: []
753
+ });
754
+ }
755
+ }
756
+ // If no blocks were created, create a default one with cleaned content
757
+ if (blocks.length === 0 && htmlContent.trim()) {
758
+ const cleanContent = htmlContent.replace(/<[^>]+>/g, '').trim();
759
+ if (cleanContent) {
760
+ blocks.push({
761
+ _type: 'block',
762
+ _key: this.generateBlockKey(),
763
+ style: 'normal',
764
+ children: this.parseInlineContent(cleanContent),
765
+ markDefs: []
766
+ });
767
+ }
768
+ }
769
+ return blocks;
770
+ }
771
+ /**
772
+ * Parse inline content and handle formatting
773
+ */
774
+ parseInlineContent(content) {
775
+ const children = [];
776
+ // Remove HTML tags and extract text with basic formatting
777
+ const cleanContent = content
778
+ .replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
779
+ .replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
780
+ .replace(/<u[^>]*>(.*?)<\/u>/gi, '_$1_')
781
+ .replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
782
+ .replace(/<[^>]+>/g, '') // Remove remaining HTML tags
783
+ .replace(/&lt;/g, '<')
784
+ .replace(/&gt;/g, '>')
785
+ .replace(/&amp;/g, '&')
786
+ .replace(/&quot;/g, '"')
787
+ .replace(/&#39;/g, "'");
788
+ if (cleanContent.trim()) {
789
+ children.push({
790
+ _type: 'span',
791
+ _key: this.generateSpanKey(),
792
+ text: cleanContent,
793
+ marks: []
794
+ });
795
+ }
796
+ return children;
797
+ }
798
+ /**
799
+ * Get block style from HTML tag name
800
+ */
801
+ getBlockStyle(tagName) {
802
+ const styleMap = {
803
+ 'h1': 'h1',
804
+ 'h2': 'h2',
805
+ 'h3': 'h3',
806
+ 'h4': 'h4',
807
+ 'h5': 'h5',
808
+ 'h6': 'h6',
809
+ 'blockquote': 'blockquote',
810
+ 'p': 'normal',
811
+ 'div': 'normal',
812
+ 'li': 'normal'
813
+ };
814
+ return styleMap[tagName] || 'normal';
815
+ }
816
+ /**
817
+ * Check if content appears to be HTML
818
+ */
819
+ isHtmlContent(content) {
820
+ return /<[^>]+>/.test(content);
821
+ }
822
+ /**
823
+ * Generate a new document ID based on original ID and target language
824
+ */
825
+ generateNewDocumentId(originalId, targetLanguage) {
826
+ // Strip 'drafts.' prefix if present to generate clean base IDs
827
+ const cleanId = originalId.startsWith('drafts.')
828
+ ? originalId.replace(/^drafts\./, '')
829
+ : originalId;
830
+ const timestamp = Date.now();
831
+ const suffix = targetLanguage ? `-${targetLanguage}` : '-translated';
832
+ return `${cleanId}${suffix}-${timestamp}`;
833
+ }
834
+ /**
835
+ * Generate a unique key for portable text blocks
836
+ */
837
+ generateBlockKey() {
838
+ return Math.random().toString(36).substr(2, 9);
839
+ }
840
+ /**
841
+ * Generate a unique key for spans
842
+ */
843
+ generateSpanKey() {
844
+ return Math.random().toString(36).substr(2, 9);
845
+ }
846
+ /**
847
+ * Validate document structure before creation
848
+ * Requirements: 3.4
849
+ */
850
+ validateDocumentStructure(document) {
851
+ const errors = [];
852
+ // Check required fields
853
+ if (!document._id) {
854
+ errors.push('Document must have an _id');
855
+ }
856
+ if (!document._type) {
857
+ errors.push('Document must have a _type');
858
+ }
859
+ // Validate portable text fields
860
+ const portableTextFields = ['body', 'content', 'text', 'description'];
861
+ for (const field of portableTextFields) {
862
+ if (document[field] && Array.isArray(document[field])) {
863
+ const validationResult = this.validatePortableTextArray(document[field], field);
864
+ if (!validationResult.isValid) {
865
+ errors.push(...validationResult.errors);
866
+ }
867
+ }
868
+ }
869
+ // Validate slug format
870
+ if (document.slug) {
871
+ if (typeof document.slug === 'object' && document.slug !== null && '_type' in document.slug) {
872
+ const slugObj = document.slug;
873
+ if (slugObj._type !== 'slug') {
874
+ errors.push('Slug object must have _type of "slug"');
875
+ }
876
+ if (!slugObj.current) {
877
+ errors.push('Slug object must have a current value');
878
+ }
879
+ }
880
+ }
881
+ return {
882
+ isValid: errors.length === 0,
883
+ errors
884
+ };
885
+ }
886
+ /**
887
+ * Validate portable text array structure
888
+ */
889
+ validatePortableTextArray(blocks, fieldName) {
890
+ const errors = [];
891
+ if (!Array.isArray(blocks)) {
892
+ errors.push(`${fieldName} must be an array`);
893
+ return { isValid: false, errors };
894
+ }
895
+ for (let i = 0; i < blocks.length; i++) {
896
+ const block = blocks[i];
897
+ if (!block || typeof block !== 'object') {
898
+ errors.push(`${fieldName}[${i}] must be an object`);
899
+ continue;
900
+ }
901
+ const blockObj = block;
902
+ if (!blockObj._type) {
903
+ errors.push(`${fieldName}[${i}] must have a _type`);
904
+ }
905
+ if (!blockObj._key) {
906
+ errors.push(`${fieldName}[${i}] must have a _key`);
907
+ }
908
+ if (blockObj._type === 'block') {
909
+ if (!Array.isArray(blockObj.children)) {
910
+ errors.push(`${fieldName}[${i}] block must have children array`);
911
+ }
912
+ else {
913
+ // Validate children
914
+ const children = blockObj.children;
915
+ for (let j = 0; j < children.length; j++) {
916
+ const child = children[j];
917
+ if (!child || typeof child !== 'object') {
918
+ errors.push(`${fieldName}[${i}].children[${j}] must be an object`);
919
+ continue;
920
+ }
921
+ const childObj = child;
922
+ if (!childObj._type) {
923
+ errors.push(`${fieldName}[${i}].children[${j}] must have a _type`);
924
+ }
925
+ if (!childObj._key) {
926
+ errors.push(`${fieldName}[${i}].children[${j}] must have a _key`);
927
+ }
928
+ }
929
+ }
930
+ }
931
+ }
932
+ return {
933
+ isValid: errors.length === 0,
934
+ errors
935
+ };
936
+ }
937
+ }
938
+ //# sourceMappingURL=documentCreationService.js.map