Skip to content

Commit b3fa093

Browse files
Merge pull request #76 from contentstack/development
DX | 07-07-2025 | Release
2 parents 8e99c44 + 5899cd9 commit b3fa093

File tree

6 files changed

+105
-46
lines changed

6 files changed

+105
-46
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@contentstack/datasync-manager",
33
"author": "Contentstack LLC <[email protected]>",
4-
"version": "2.0.10",
4+
"version": "2.1.0",
55
"description": "The primary module of Contentstack DataSync. Syncs Contentstack data with your server using Contentstack Sync API",
66
"main": "dist/index.js",
77
"dependencies": {

src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,9 @@ export const config = {
127127
saveFailedItems: true,
128128
saveFilteredItems: true,
129129
},
130+
checkpoint: {
131+
enabled: false, // Set to true if you want to enable checkpoint
132+
filePath: ".checkpoint",
133+
preserve: false // Set to true if you want to preserve the checkpoint file during clean operation
134+
},
130135
}

src/core/index.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* Copyright (c) 2019 Contentstack LLC
44
* MIT Licensed
55
*/
6-
6+
import * as fs from 'fs'
7+
import * as path from 'path'
78
import Debug from 'debug'
89
import { EventEmitter } from 'events'
910
import { cloneDeep, remove } from 'lodash'
@@ -16,6 +17,7 @@ import { map } from '../util/promise.map'
1617
import { netConnectivityIssues } from './inet'
1718
import { Q as Queue } from './q'
1819
import { getToken, saveCheckpoint } from './token-management'
20+
import { sanitizePath } from '../plugins/helper'
1921

2022
interface IQueryString {
2123
init?: true,
@@ -46,6 +48,11 @@ interface IToken {
4648
name: string
4749
token: string
4850
}
51+
interface ICheckpoint {
52+
enabled: boolean,
53+
filePath: string,
54+
preserve:boolean
55+
}
4956

5057
const debug = Debug('sync-core')
5158
const emitter = new EventEmitter()
@@ -74,6 +81,7 @@ export const init = (contentStore, assetStore) => {
7481
return new Promise((resolve, reject) => {
7582
try {
7683
Contentstack = config.contentstack
84+
const checkPointConfig: ICheckpoint = config.checkpoint
7785
const paths = config.paths
7886
const environment = Contentstack.environment || process.env.NODE_ENV || 'development'
7987
debug(`Environment: ${environment}`)
@@ -83,6 +91,7 @@ export const init = (contentStore, assetStore) => {
8391
limit: config.syncManager.limit,
8492
},
8593
}
94+
loadCheckpoint(checkPointConfig, paths);
8695
if (typeof Contentstack.sync_token === 'string' && Contentstack.sync_token.length !== 0) {
8796
request.qs.sync_token = Contentstack.sync_token
8897
} else if (typeof Contentstack.pagination_token === 'string' && Contentstack.pagination_token.length !== 0) {
@@ -110,6 +119,44 @@ export const init = (contentStore, assetStore) => {
110119
})
111120
}
112121

122+
const loadCheckpoint = (checkPointConfig: ICheckpoint, paths: any): void => {
123+
if (!checkPointConfig?.enabled) return;
124+
125+
// Try reading checkpoint from primary path
126+
let checkpoint = readHiddenFile(paths.checkpoint);
127+
128+
// Fallback to filePath in config if not found
129+
if (!checkpoint) {
130+
const fallbackPath = path.join(
131+
sanitizePath(__dirname),
132+
sanitizePath(checkPointConfig.filePath || ".checkpoint")
133+
);
134+
checkpoint = readHiddenFile(fallbackPath);
135+
}
136+
137+
// Set sync token if checkpoint is found
138+
if (checkpoint) {
139+
debug("Found sync token in checkpoint file:", checkpoint);
140+
Contentstack.sync_token = checkpoint.token;
141+
debug("Using sync token:", Contentstack.sync_token);
142+
}
143+
};
144+
145+
146+
function readHiddenFile(filePath: string) {
147+
try {
148+
if (!fs.existsSync(filePath)) {
149+
logger.error("File does not exist:", filePath);
150+
return;
151+
}
152+
const data = fs.readFileSync(filePath, "utf8");
153+
return JSON.parse(data);
154+
} catch (err) {
155+
logger.error("Error reading file:", err);
156+
return undefined;
157+
}
158+
}
159+
113160
export const push = (data) => {
114161
Q.emit('push', data)
115162
}

src/core/plugins.ts

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,47 +19,52 @@ const pluginMethods = ['beforeSync', 'afterSync']
1919
*/
2020
export const load = (config) => {
2121
debug('Plugins load called')
22-
const pluginInstances = {
23-
external: {},
24-
internal: {},
25-
}
26-
const plugins = config.plugins || []
27-
pluginMethods.forEach((pluginMethod) => {
28-
pluginInstances.external[pluginMethod] = pluginInstances[pluginMethod] || []
29-
pluginInstances.internal[pluginMethod] = pluginInstances[pluginMethod] || []
30-
})
31-
32-
plugins.forEach((plugin) => {
33-
validatePlugin(plugin)
34-
35-
const pluginName = plugin.name
36-
const slicedName = pluginName.slice(0, 13)
37-
let isInternal = false
38-
if (slicedName === '_cs_internal_') {
39-
isInternal = true
22+
try {
23+
const pluginInstances = {
24+
external: {},
25+
internal: {},
4026
}
41-
42-
const pluginPath = normalizePluginPath(config, plugin, isInternal)
43-
const Plugin = require(pluginPath)
44-
Plugin.options = plugin.options || {}
45-
// execute/initiate plugin
46-
Plugin()
27+
const plugins = config.plugins || []
4728
pluginMethods.forEach((pluginMethod) => {
48-
if (hasIn(Plugin, pluginMethod)) {
49-
if (plugin.disabled) {
50-
// do nothing
51-
} else if (isInternal) {
52-
pluginInstances.internal[pluginMethod].push(Plugin[pluginMethod])
29+
pluginInstances.external[pluginMethod] = pluginInstances[pluginMethod] || []
30+
pluginInstances.internal[pluginMethod] = pluginInstances[pluginMethod] || []
31+
})
32+
33+
plugins.forEach((plugin) => {
34+
validatePlugin(plugin)
35+
36+
const pluginName = plugin.name
37+
const slicedName = pluginName.slice(0, 13)
38+
let isInternal = false
39+
if (slicedName === '_cs_internal_') {
40+
isInternal = true
41+
}
42+
43+
const pluginPath = normalizePluginPath(config, plugin, isInternal)
44+
const Plugin = require(pluginPath)
45+
Plugin.options = plugin.options || {}
46+
// execute/initiate plugin
47+
Plugin()
48+
pluginMethods.forEach((pluginMethod) => {
49+
if (hasIn(Plugin, pluginMethod)) {
50+
if (plugin.disabled) {
51+
// do nothing
52+
} else if (isInternal) {
53+
pluginInstances.internal[pluginMethod].push(Plugin[pluginMethod])
54+
} else {
55+
pluginInstances.external[pluginMethod].push(Plugin[pluginMethod])
56+
}
57+
debug(`${pluginMethod} loaded from ${pluginName} successfully!`)
5358
} else {
54-
pluginInstances.external[pluginMethod].push(Plugin[pluginMethod])
59+
debug(`${pluginMethod} not found in ${pluginName}`)
5560
}
56-
debug(`${pluginMethod} loaded from ${pluginName} successfully!`)
57-
} else {
58-
debug(`${pluginMethod} not found in ${pluginName}`)
59-
}
61+
})
6062
})
61-
})
62-
debug('Plugins loaded successfully!')
63-
64-
return pluginInstances
63+
debug('Plugins loaded successfully!')
64+
65+
return pluginInstances
66+
} catch (error) {
67+
debug('Error while loading plugins:', error)
68+
throw new Error(`Failed to load plugins: ${error?.message}`)
69+
}
6570
}

src/util/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,9 @@ export const formatItems = (items, config) => {
149149
items[i]._type = config.contentstack.actions.publish
150150
// extra keys
151151
items[i]._synced_at = time
152-
items[i].locale = items[i].data.publish_details.locale
153-
items[i] = merge(items[i], items[i].data)
152+
const assetLocale = items[i].data.publish_details.locale
153+
items[i] = merge(cloneDeep(items[i]), items[i].data)
154+
items[i].locale = assetLocale
154155
break
155156
case 'asset_unpublished':
156157
delete items[i].type
@@ -170,8 +171,9 @@ export const formatItems = (items, config) => {
170171
items[i]._content_type_uid = items[i].content_type_uid
171172
// extra keys
172173
items[i]._synced_at = time
173-
items[i].locale = items[i].data.publish_details.locale
174-
items[i] = merge(items[i], items[i].data)
174+
const entryLocale = items[i].data.publish_details.locale
175+
items[i] = merge(cloneDeep(items[i]), items[i].data)
176+
items[i].locale = entryLocale
175177
break
176178
case 'entry_unpublished':
177179
delete items[i].type

0 commit comments

Comments
 (0)