@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.
- package/LICENSE +21 -0
- package/README.md +482 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/actions/bulkTranslate.d.ts +7 -0
- package/dist/actions/bulkTranslate.d.ts.map +1 -0
- package/dist/actions/bulkTranslate.js +543 -0
- package/dist/actions/bulkTranslate.js.map +1 -0
- package/dist/actions/manageDNTFields.d.ts +12 -0
- package/dist/actions/manageDNTFields.d.ts.map +1 -0
- package/dist/actions/manageDNTFields.js +100 -0
- package/dist/actions/manageDNTFields.js.map +1 -0
- package/dist/actions/translateDocument.d.ts +7 -0
- package/dist/actions/translateDocument.d.ts.map +1 -0
- package/dist/actions/translateDocument.js +256 -0
- package/dist/actions/translateDocument.js.map +1 -0
- package/dist/components/RadioWithDefault.d.ts +17 -0
- package/dist/components/RadioWithDefault.d.ts.map +1 -0
- package/dist/components/RadioWithDefault.js +29 -0
- package/dist/components/RadioWithDefault.js.map +1 -0
- package/dist/components/auth/AuthNavbar.d.ts +18 -0
- package/dist/components/auth/AuthNavbar.d.ts.map +1 -0
- package/dist/components/auth/AuthNavbar.js +15 -0
- package/dist/components/auth/AuthNavbar.js.map +1 -0
- package/dist/components/auth/AuthStatus.d.ts +27 -0
- package/dist/components/auth/AuthStatus.d.ts.map +1 -0
- package/dist/components/auth/AuthStatus.js +82 -0
- package/dist/components/auth/AuthStatus.js.map +1 -0
- package/dist/components/auth/AuthStatusWrapper.d.ts +15 -0
- package/dist/components/auth/AuthStatusWrapper.d.ts.map +1 -0
- package/dist/components/auth/AuthStatusWrapper.js +39 -0
- package/dist/components/auth/AuthStatusWrapper.js.map +1 -0
- package/dist/components/auth/MigrationPrompt.d.ts +20 -0
- package/dist/components/auth/MigrationPrompt.d.ts.map +1 -0
- package/dist/components/auth/MigrationPrompt.js +16 -0
- package/dist/components/auth/MigrationPrompt.js.map +1 -0
- package/dist/components/auth/MigrationPromptWrapper.d.ts +14 -0
- package/dist/components/auth/MigrationPromptWrapper.d.ts.map +1 -0
- package/dist/components/auth/MigrationPromptWrapper.js +16 -0
- package/dist/components/auth/MigrationPromptWrapper.js.map +1 -0
- package/dist/components/auth/OAuthCallback.d.ts +20 -0
- package/dist/components/auth/OAuthCallback.d.ts.map +1 -0
- package/dist/components/auth/OAuthCallback.js +12 -0
- package/dist/components/auth/OAuthCallback.js.map +1 -0
- package/dist/components/auth/index.d.ts +15 -0
- package/dist/components/auth/index.d.ts.map +1 -0
- package/dist/components/auth/index.js +12 -0
- package/dist/components/auth/index.js.map +1 -0
- package/dist/components/config/LocaleConfigTool.d.ts +17 -0
- package/dist/components/config/LocaleConfigTool.d.ts.map +1 -0
- package/dist/components/config/LocaleConfigTool.js +186 -0
- package/dist/components/config/LocaleConfigTool.js.map +1 -0
- package/dist/components/config/LocaleConfigToolWrapper.d.ts +13 -0
- package/dist/components/config/LocaleConfigToolWrapper.d.ts.map +1 -0
- package/dist/components/config/LocaleConfigToolWrapper.js +26 -0
- package/dist/components/config/LocaleConfigToolWrapper.js.map +1 -0
- package/dist/components/config/OAuthConfig.d.ts +26 -0
- package/dist/components/config/OAuthConfig.d.ts.map +1 -0
- package/dist/components/config/OAuthConfig.js +152 -0
- package/dist/components/config/OAuthConfig.js.map +1 -0
- package/dist/components/config/OAuthConfigWrapper.d.ts +13 -0
- package/dist/components/config/OAuthConfigWrapper.d.ts.map +1 -0
- package/dist/components/config/OAuthConfigWrapper.js +41 -0
- package/dist/components/config/OAuthConfigWrapper.js.map +1 -0
- package/dist/components/config/PasswordInput.d.ts +14 -0
- package/dist/components/config/PasswordInput.d.ts.map +1 -0
- package/dist/components/config/PasswordInput.js +23 -0
- package/dist/components/config/PasswordInput.js.map +1 -0
- package/dist/components/config/index.d.ts +9 -0
- package/dist/components/config/index.d.ts.map +1 -0
- package/dist/components/config/index.js +8 -0
- package/dist/components/config/index.js.map +1 -0
- package/dist/components/config/localeConfigToolDefinition.d.ts +13 -0
- package/dist/components/config/localeConfigToolDefinition.d.ts.map +1 -0
- package/dist/components/config/localeConfigToolDefinition.js +19 -0
- package/dist/components/config/localeConfigToolDefinition.js.map +1 -0
- package/dist/components/config/oauthConfigToolDefinition.d.ts +13 -0
- package/dist/components/config/oauthConfigToolDefinition.d.ts.map +1 -0
- package/dist/components/config/oauthConfigToolDefinition.js +19 -0
- package/dist/components/config/oauthConfigToolDefinition.js.map +1 -0
- package/dist/components/dialogs/ConfirmationDialog.d.ts +21 -0
- package/dist/components/dialogs/ConfirmationDialog.d.ts.map +1 -0
- package/dist/components/dialogs/ConfirmationDialog.js +28 -0
- package/dist/components/dialogs/ConfirmationDialog.js.map +1 -0
- package/dist/components/dialogs/ErrorDialog.d.ts +21 -0
- package/dist/components/dialogs/ErrorDialog.d.ts.map +1 -0
- package/dist/components/dialogs/ErrorDialog.js +28 -0
- package/dist/components/dialogs/ErrorDialog.js.map +1 -0
- package/dist/components/dialogs/LocaleSelectionDialog.d.ts +41 -0
- package/dist/components/dialogs/LocaleSelectionDialog.d.ts.map +1 -0
- package/dist/components/dialogs/LocaleSelectionDialog.js +117 -0
- package/dist/components/dialogs/LocaleSelectionDialog.js.map +1 -0
- package/dist/components/dialogs/SuccessDialog.d.ts +19 -0
- package/dist/components/dialogs/SuccessDialog.d.ts.map +1 -0
- package/dist/components/dialogs/SuccessDialog.js +37 -0
- package/dist/components/dialogs/SuccessDialog.js.map +1 -0
- package/dist/components/dialogs/index.d.ts +12 -0
- package/dist/components/dialogs/index.d.ts.map +1 -0
- package/dist/components/dialogs/index.js +8 -0
- package/dist/components/dialogs/index.js.map +1 -0
- package/dist/components/dnt/DNTFieldBadge.d.ts +16 -0
- package/dist/components/dnt/DNTFieldBadge.d.ts.map +1 -0
- package/dist/components/dnt/DNTFieldBadge.js +56 -0
- package/dist/components/dnt/DNTFieldBadge.js.map +1 -0
- package/dist/components/dnt/DNTFieldComponent.d.ts +17 -0
- package/dist/components/dnt/DNTFieldComponent.d.ts.map +1 -0
- package/dist/components/dnt/DNTFieldComponent.js +21 -0
- package/dist/components/dnt/DNTFieldComponent.js.map +1 -0
- package/dist/components/dnt/DNTFieldInput.d.ts +14 -0
- package/dist/components/dnt/DNTFieldInput.d.ts.map +1 -0
- package/dist/components/dnt/DNTFieldInput.js +76 -0
- package/dist/components/dnt/DNTFieldInput.js.map +1 -0
- package/dist/components/dnt/index.d.ts +7 -0
- package/dist/components/dnt/index.d.ts.map +1 -0
- package/dist/components/dnt/index.js +7 -0
- package/dist/components/dnt/index.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/pluginConfig.d.ts +163 -0
- package/dist/config/pluginConfig.d.ts.map +1 -0
- package/dist/config/pluginConfig.js +548 -0
- package/dist/config/pluginConfig.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +137 -0
- package/dist/plugin.js.map +1 -0
- package/dist/services/authStateManager.d.ts +94 -0
- package/dist/services/authStateManager.d.ts.map +1 -0
- package/dist/services/authStateManager.js +208 -0
- package/dist/services/authStateManager.js.map +1 -0
- package/dist/services/contentExtractor.d.ts +95 -0
- package/dist/services/contentExtractor.d.ts.map +1 -0
- package/dist/services/contentExtractor.js +516 -0
- package/dist/services/contentExtractor.js.map +1 -0
- package/dist/services/dialogService.d.ts +96 -0
- package/dist/services/dialogService.d.ts.map +1 -0
- package/dist/services/dialogService.js +244 -0
- package/dist/services/dialogService.js.map +1 -0
- package/dist/services/dntServiceManager.d.ts +44 -0
- package/dist/services/dntServiceManager.d.ts.map +1 -0
- package/dist/services/dntServiceManager.js +74 -0
- package/dist/services/dntServiceManager.js.map +1 -0
- package/dist/services/dntStorageAdapter.d.ts +73 -0
- package/dist/services/dntStorageAdapter.d.ts.map +1 -0
- package/dist/services/dntStorageAdapter.js +192 -0
- package/dist/services/dntStorageAdapter.js.map +1 -0
- package/dist/services/documentCreationService.d.ts +139 -0
- package/dist/services/documentCreationService.d.ts.map +1 -0
- package/dist/services/documentCreationService.js +938 -0
- package/dist/services/documentCreationService.js.map +1 -0
- package/dist/services/localeService.d.ts +160 -0
- package/dist/services/localeService.d.ts.map +1 -0
- package/dist/services/localeService.js +300 -0
- package/dist/services/localeService.js.map +1 -0
- package/dist/services/localeStorageAdapter.d.ts +42 -0
- package/dist/services/localeStorageAdapter.d.ts.map +1 -0
- package/dist/services/localeStorageAdapter.js +107 -0
- package/dist/services/localeStorageAdapter.js.map +1 -0
- package/dist/services/oauthConfigStorage.d.ts +46 -0
- package/dist/services/oauthConfigStorage.d.ts.map +1 -0
- package/dist/services/oauthConfigStorage.js +122 -0
- package/dist/services/oauthConfigStorage.js.map +1 -0
- package/dist/services/oauthService.d.ts +48 -0
- package/dist/services/oauthService.d.ts.map +1 -0
- package/dist/services/oauthService.js +71 -0
- package/dist/services/oauthService.js.map +1 -0
- package/dist/services/oauthServiceManager.d.ts +189 -0
- package/dist/services/oauthServiceManager.d.ts.map +1 -0
- package/dist/services/oauthServiceManager.js +380 -0
- package/dist/services/oauthServiceManager.js.map +1 -0
- package/dist/services/tokenStorage.d.ts +54 -0
- package/dist/services/tokenStorage.d.ts.map +1 -0
- package/dist/services/tokenStorage.js +140 -0
- package/dist/services/tokenStorage.js.map +1 -0
- package/dist/services/translationService.d.ts +374 -0
- package/dist/services/translationService.d.ts.map +1 -0
- package/dist/services/translationService.js +687 -0
- package/dist/services/translationService.js.map +1 -0
- package/dist/services/unifiedConfigStorage.d.ts +124 -0
- package/dist/services/unifiedConfigStorage.d.ts.map +1 -0
- package/dist/services/unifiedConfigStorage.js +304 -0
- package/dist/services/unifiedConfigStorage.js.map +1 -0
- package/dist/test-utils.d.ts +9 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +13 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/types/dialog.d.ts +107 -0
- package/dist/types/dialog.d.ts.map +1 -0
- package/dist/types/dialog.js +6 -0
- package/dist/types/dialog.js.map +1 -0
- package/dist/types/dnt.d.ts +84 -0
- package/dist/types/dnt.d.ts.map +1 -0
- package/dist/types/dnt.js +5 -0
- package/dist/types/dnt.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/locale.d.ts +116 -0
- package/dist/types/locale.d.ts.map +1 -0
- package/dist/types/locale.js +189 -0
- package/dist/types/locale.js.map +1 -0
- package/dist/types/oauth.d.ts +90 -0
- package/dist/types/oauth.d.ts.map +1 -0
- package/dist/types/oauth.js +62 -0
- package/dist/types/oauth.js.map +1 -0
- package/dist/types/pluginConfig.d.ts +45 -0
- package/dist/types/pluginConfig.d.ts.map +1 -0
- package/dist/types/pluginConfig.js +6 -0
- package/dist/types/pluginConfig.js.map +1 -0
- package/dist/types/translation.d.ts +122 -0
- package/dist/types/translation.d.ts.map +1 -0
- package/dist/types/translation.js +6 -0
- package/dist/types/translation.js.map +1 -0
- package/dist/utils/htmlFormatter.d.ts +66 -0
- package/dist/utils/htmlFormatter.d.ts.map +1 -0
- package/dist/utils/htmlFormatter.js +191 -0
- package/dist/utils/htmlFormatter.js.map +1 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +16 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +105 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +229 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/oauthErrorFeedback.d.ts +76 -0
- package/dist/utils/oauthErrorFeedback.d.ts.map +1 -0
- package/dist/utils/oauthErrorFeedback.js +134 -0
- package/dist/utils/oauthErrorFeedback.js.map +1 -0
- package/dist/utils/oauthLogger.d.ts +176 -0
- package/dist/utils/oauthLogger.d.ts.map +1 -0
- package/dist/utils/oauthLogger.js +282 -0
- package/dist/utils/oauthLogger.js.map +1 -0
- package/dist/utils/validator.d.ts +67 -0
- package/dist/utils/validator.d.ts.map +1 -0
- package/dist/utils/validator.js +390 -0
- package/dist/utils/validator.js.map +1 -0
- 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(/</g, '<')
|
|
784
|
+
.replace(/>/g, '>')
|
|
785
|
+
.replace(/&/g, '&')
|
|
786
|
+
.replace(/"/g, '"')
|
|
787
|
+
.replace(/'/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
|