create-mercato-app 0.5.1-develop.2744.9c8be0dd93 → 0.5.1-develop.2762.90c271efe2
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/package.json +1 -1
- package/template/.env.example +2 -2
- package/template/scripts/dev-runtime.mjs +42 -8
- package/template/scripts/dev-spawn-utils.mjs +31 -0
- package/template/scripts/setup.mjs +10 -0
- package/template/src/modules/example/commands/todos.ts +2 -2
- package/template/src/modules/example_customers_sync/api/example-customers-sync/mappings/route.ts +18 -9
- package/template/src/modules/example_customers_sync/lib/sync.ts +51 -9
- /package/dist/{lib/templates → templates}/modules-ts.template +0 -0
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -178,10 +178,10 @@ CACHE_TTL=300000
|
|
|
178
178
|
#CACHE_REDIS_URL=redis://localhost:6379
|
|
179
179
|
|
|
180
180
|
# SQLite configuration (for sqlite strategy)
|
|
181
|
-
CACHE_SQLITE_PATH
|
|
181
|
+
CACHE_SQLITE_PATH=./.mercato/cache/cache.db
|
|
182
182
|
|
|
183
183
|
# JSON file configuration (for jsonfile strategy)
|
|
184
|
-
#CACHE_JSON_FILE_PATH
|
|
184
|
+
#CACHE_JSON_FILE_PATH=./.mercato/cache/cache.json
|
|
185
185
|
|
|
186
186
|
# Database pooling settings
|
|
187
187
|
DB_POOL_MIN=5
|
|
@@ -55,9 +55,9 @@ const {
|
|
|
55
55
|
stripAnsi,
|
|
56
56
|
wrapListLines,
|
|
57
57
|
} = await import(resolveSplashHelpersImport())
|
|
58
|
-
const { resolveSpawnCommand } = await import(resolveSpawnUtilsImport())
|
|
58
|
+
const { resolveProjectBinary, resolveSpawnCommand } = await import(resolveSpawnUtilsImport())
|
|
59
59
|
|
|
60
|
-
const command = process.platform === 'win32' ? 'mercato.cmd' : 'mercato'
|
|
60
|
+
const command = resolveProjectBinary(process.platform === 'win32' ? 'mercato.cmd' : 'mercato')
|
|
61
61
|
const classic = process.argv.includes('--classic') || isEnabledEnvFlag(process.env.OM_DEV_CLASSIC)
|
|
62
62
|
const verbose = !classic && (process.argv.includes('--verbose') || process.env.MERCATO_DEV_OUTPUT === 'verbose')
|
|
63
63
|
const rawPassthrough = classic || verbose
|
|
@@ -412,10 +412,10 @@ function spawnMercato(args) {
|
|
|
412
412
|
return child
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
function waitForExit(child) {
|
|
415
|
+
function waitForExit(child, label = 'Child process') {
|
|
416
416
|
return new Promise((resolve) => {
|
|
417
417
|
child.on('exit', (code, signal) => {
|
|
418
|
-
resolve({ code, signal })
|
|
418
|
+
resolve({ label, code, signal })
|
|
419
419
|
})
|
|
420
420
|
})
|
|
421
421
|
}
|
|
@@ -441,6 +441,32 @@ function resolveChildExitCode(result, fallback = 1) {
|
|
|
441
441
|
return fallback
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
function formatChildExitStatus(result) {
|
|
445
|
+
if (typeof result?.code === 'number') {
|
|
446
|
+
return `exit code ${result.code}`
|
|
447
|
+
}
|
|
448
|
+
if (result?.signal) {
|
|
449
|
+
return `signal ${result.signal}`
|
|
450
|
+
}
|
|
451
|
+
return 'an unknown status'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveUnexpectedExitCode(result) {
|
|
455
|
+
const exitCode = resolveChildExitCode(result, 1)
|
|
456
|
+
return exitCode === 0 ? 1 : exitCode
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function reportUnexpectedChildExit(result) {
|
|
460
|
+
const message = `❌ ${result?.label ?? 'Child process'} exited unexpectedly with ${formatChildExitStatus(result)}`
|
|
461
|
+
console.error(message)
|
|
462
|
+
rememberRawLog(message)
|
|
463
|
+
publishRuntimeFailure(message, {
|
|
464
|
+
progressCurrent: splashState.progressCurrent >= runtimeProgressCurrent ? splashState.progressCurrent : runtimeProgressCurrent,
|
|
465
|
+
progressLabel: splashState.progressLabel || startupProgress.label,
|
|
466
|
+
failureLines: [...collectRuntimeFailureLines(), message].slice(-10),
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
444
470
|
function joinBaseUrl(baseUrl, pathname) {
|
|
445
471
|
return `${String(baseUrl ?? '').replace(/\/$/, '')}${pathname}`
|
|
446
472
|
}
|
|
@@ -1521,12 +1547,16 @@ async function runClassicRuntime() {
|
|
|
1521
1547
|
|
|
1522
1548
|
const watch = spawnMercato(['generate', 'watch', '--skip-initial'])
|
|
1523
1549
|
const server = spawnMercato(['server', 'dev'])
|
|
1524
|
-
const result = await Promise.race([
|
|
1550
|
+
const result = await Promise.race([
|
|
1551
|
+
waitForExit(watch, 'Generator watch'),
|
|
1552
|
+
waitForExit(server, 'App runtime'),
|
|
1553
|
+
])
|
|
1525
1554
|
if (isGracefulShutdownResult(result)) {
|
|
1526
1555
|
return
|
|
1527
1556
|
}
|
|
1528
1557
|
|
|
1529
|
-
|
|
1558
|
+
reportUnexpectedChildExit(result)
|
|
1559
|
+
shutdown(resolveUnexpectedExitCode(result))
|
|
1530
1560
|
}
|
|
1531
1561
|
|
|
1532
1562
|
if (classic) {
|
|
@@ -1541,7 +1571,11 @@ printRuntimePackagesSummary()
|
|
|
1541
1571
|
const watch = startFilteredChild(['generate', 'watch', '--skip-initial'], 'Generator watch', classifyWatchLine)
|
|
1542
1572
|
const server = startFilteredChild(['server', 'dev'], 'App runtime', classifyServerLine)
|
|
1543
1573
|
|
|
1544
|
-
const result = await Promise.race([
|
|
1574
|
+
const result = await Promise.race([
|
|
1575
|
+
waitForExit(watch, 'Generator watch'),
|
|
1576
|
+
waitForExit(server, 'App runtime'),
|
|
1577
|
+
])
|
|
1545
1578
|
if (!isGracefulShutdownResult(result)) {
|
|
1546
|
-
|
|
1579
|
+
reportUnexpectedChildExit(result)
|
|
1580
|
+
shutdown(resolveUnexpectedExitCode(result))
|
|
1547
1581
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
1
4
|
function isWindowsCmdScript(command, platform = process.platform) {
|
|
2
5
|
return platform === 'win32' && /\.(cmd|bat)$/i.test(String(command))
|
|
3
6
|
}
|
|
@@ -23,6 +26,34 @@ function assertWindowsCmdSafeValue(value, label) {
|
|
|
23
26
|
return stringValue
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
export function resolveProjectBinary(command, options = {}) {
|
|
30
|
+
const safeCommand = assertProcessSafeValue(command, 'Process command')
|
|
31
|
+
const cwd = options.cwd ?? process.cwd()
|
|
32
|
+
const platform = options.platform ?? process.platform
|
|
33
|
+
|
|
34
|
+
if (path.isAbsolute(safeCommand) || safeCommand.includes('/') || safeCommand.includes('\\')) {
|
|
35
|
+
return safeCommand
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const binDir = path.join(cwd, 'node_modules', '.bin')
|
|
39
|
+
const candidates = platform === 'win32'
|
|
40
|
+
? [
|
|
41
|
+
path.join(binDir, safeCommand),
|
|
42
|
+
path.join(binDir, `${safeCommand}.cmd`),
|
|
43
|
+
path.join(binDir, `${safeCommand}.bat`),
|
|
44
|
+
path.join(binDir, `${safeCommand}.exe`),
|
|
45
|
+
]
|
|
46
|
+
: [path.join(binDir, safeCommand)]
|
|
47
|
+
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (fs.existsSync(candidate)) {
|
|
50
|
+
return candidate
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return safeCommand
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
export function resolveSpawnCommand(command, commandArgs = [], options = {}) {
|
|
27
58
|
const platform = options.platform ?? process.platform
|
|
28
59
|
const safeCommand = assertProcessSafeValue(command, 'Process command')
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
2
3
|
|
|
3
4
|
const reinstall = process.argv.includes('--reinstall')
|
|
4
5
|
const classic = process.argv.includes('--classic')
|
|
6
|
+
|
|
7
|
+
if (!existsSync('node_modules/cross-spawn')) {
|
|
8
|
+
const bootstrap = spawnSync('yarn', ['install'], {
|
|
9
|
+
stdio: 'inherit',
|
|
10
|
+
shell: process.platform === 'win32',
|
|
11
|
+
})
|
|
12
|
+
if (bootstrap.status !== 0) process.exit(bootstrap.status ?? 1)
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
const result = spawnSync(
|
|
6
16
|
process.execPath,
|
|
7
17
|
['./scripts/dev.mjs', '--setup', ...(reinstall ? ['--reinstall'] : []), ...(classic ? ['--classic'] : [])],
|
|
@@ -27,13 +27,13 @@ import {
|
|
|
27
27
|
|
|
28
28
|
export const todoCreateSchema = z.object({
|
|
29
29
|
id: z.string().uuid().optional(),
|
|
30
|
-
title: z.string().min(1),
|
|
30
|
+
title: z.string().min(1).max(200),
|
|
31
31
|
is_done: z.boolean().optional(),
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
export const todoUpdateSchema = z.object({
|
|
35
35
|
id: z.string().uuid(),
|
|
36
|
-
title: z.string().min(1).optional(),
|
|
36
|
+
title: z.string().min(1).max(200).optional(),
|
|
37
37
|
is_done: z.boolean().optional(),
|
|
38
38
|
})
|
|
39
39
|
|
package/template/src/modules/example_customers_sync/api/example-customers-sync/mappings/route.ts
CHANGED
|
@@ -24,11 +24,11 @@ type MappingRow = {
|
|
|
24
24
|
interaction_id: string
|
|
25
25
|
todo_id: string
|
|
26
26
|
sync_status: string
|
|
27
|
-
last_synced_at: Date | null
|
|
27
|
+
last_synced_at: Date | string | null
|
|
28
28
|
last_error: string | null
|
|
29
|
-
source_updated_at: Date | null
|
|
30
|
-
created_at: Date
|
|
31
|
-
updated_at: Date
|
|
29
|
+
source_updated_at: Date | string | null
|
|
30
|
+
created_at: Date | string
|
|
31
|
+
updated_at: Date | string
|
|
32
32
|
organization_id: string
|
|
33
33
|
tenant_id: string
|
|
34
34
|
}
|
|
@@ -53,6 +53,15 @@ function decodeCursor(token: string | undefined): CursorPayload | null {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function toIsoString(value: Date | string | null | undefined): string | null {
|
|
57
|
+
if (!value) return null
|
|
58
|
+
if (value instanceof Date) {
|
|
59
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString()
|
|
60
|
+
}
|
|
61
|
+
const parsed = new Date(value)
|
|
62
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
export async function GET(request: Request) {
|
|
57
66
|
const { translate } = await resolveTranslations()
|
|
58
67
|
try {
|
|
@@ -163,11 +172,11 @@ export async function GET(request: Request) {
|
|
|
163
172
|
interactionId: row.interaction_id,
|
|
164
173
|
todoId: row.todo_id,
|
|
165
174
|
syncStatus: row.sync_status,
|
|
166
|
-
lastSyncedAt: row.last_synced_at
|
|
175
|
+
lastSyncedAt: toIsoString(row.last_synced_at),
|
|
167
176
|
lastError: row.last_error ?? null,
|
|
168
|
-
sourceUpdatedAt: row.source_updated_at
|
|
169
|
-
createdAt: row.created_at
|
|
170
|
-
updatedAt: row.updated_at
|
|
177
|
+
sourceUpdatedAt: toIsoString(row.source_updated_at),
|
|
178
|
+
createdAt: toIsoString(row.created_at),
|
|
179
|
+
updatedAt: toIsoString(row.updated_at),
|
|
171
180
|
organizationId: row.organization_id,
|
|
172
181
|
tenantId: row.tenant_id,
|
|
173
182
|
exampleHref: `/backend/todos/${encodeURIComponent(row.todo_id)}/edit`,
|
|
@@ -186,7 +195,7 @@ export async function GET(request: Request) {
|
|
|
186
195
|
|
|
187
196
|
return NextResponse.json({
|
|
188
197
|
items,
|
|
189
|
-
...(last ? { nextCursor: encodeCursor({ updatedAt: last.updated_at.toISOString(), id: last.id }) } : {}),
|
|
198
|
+
...(last ? { nextCursor: encodeCursor({ updatedAt: toIsoString(last.updated_at) ?? new Date(0).toISOString(), id: last.id }) } : {}),
|
|
190
199
|
})
|
|
191
200
|
} catch (error) {
|
|
192
201
|
if (error instanceof z.ZodError) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
1
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import { type Kysely } from 'kysely'
|
|
3
4
|
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
5
|
+
import '@open-mercato/core/modules/customers/commands/index'
|
|
4
6
|
import { loadCustomFieldSnapshot } from '@open-mercato/shared/lib/commands/customFieldSnapshots'
|
|
5
7
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
8
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
@@ -66,7 +68,7 @@ type LegacyExampleTodoLinkRow = {
|
|
|
66
68
|
entityId: string
|
|
67
69
|
todoId: string
|
|
68
70
|
createdByUserId: string | null
|
|
69
|
-
createdAt: Date
|
|
71
|
+
createdAt: Date | string
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export type ExampleCustomersSyncReconcileItem = {
|
|
@@ -92,6 +94,8 @@ type CursorPayload = {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
const DEFAULT_TASK_TITLE = 'Untitled task'
|
|
97
|
+
const LEGACY_INBOUND_BOOTSTRAP_ATTEMPTS = 10
|
|
98
|
+
const LEGACY_INBOUND_BOOTSTRAP_DELAY_MS = 100
|
|
95
99
|
|
|
96
100
|
function isSyncOriginFromBridge(syncOrigin: unknown): boolean {
|
|
97
101
|
return typeof syncOrigin === 'string' && syncOrigin.startsWith('example_customers_sync:')
|
|
@@ -110,6 +114,10 @@ function parseDateOrNull(value: string | Date | null | undefined): Date | null {
|
|
|
110
114
|
return Number.isNaN(parsed.getTime()) ? null : parsed
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
function toIsoString(value: Date | string | null | undefined): string | null {
|
|
118
|
+
return parseDateOrNull(value)?.toISOString() ?? null
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
function trimErrorMessage(value: unknown): string {
|
|
114
122
|
const message = value instanceof Error ? value.message : String(value ?? 'Unknown sync error')
|
|
115
123
|
return message.length > 2000 ? `${message.slice(0, 1997)}...` : message
|
|
@@ -394,6 +402,38 @@ async function loadLegacyExampleTodoLinkRow(
|
|
|
394
402
|
}
|
|
395
403
|
}
|
|
396
404
|
|
|
405
|
+
async function waitForLegacyExampleTodoLinkRow(
|
|
406
|
+
em: EntityManager,
|
|
407
|
+
scope: ExampleCustomersSyncScope,
|
|
408
|
+
todoId: string,
|
|
409
|
+
): Promise<LegacyExampleTodoLinkRow | null> {
|
|
410
|
+
for (let attempt = 0; attempt < LEGACY_INBOUND_BOOTSTRAP_ATTEMPTS; attempt += 1) {
|
|
411
|
+
const link = await loadLegacyExampleTodoLinkRow(em, scope, todoId)
|
|
412
|
+
if (link) return link
|
|
413
|
+
if (attempt < LEGACY_INBOUND_BOOTSTRAP_ATTEMPTS - 1) {
|
|
414
|
+
await sleep(LEGACY_INBOUND_BOOTSTRAP_DELAY_MS)
|
|
415
|
+
em.clear()
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function waitForExampleTodoSnapshot(
|
|
422
|
+
em: EntityManager,
|
|
423
|
+
scope: ExampleCustomersSyncScope,
|
|
424
|
+
todoId: string,
|
|
425
|
+
): Promise<ExampleTodoSnapshot | null> {
|
|
426
|
+
for (let attempt = 0; attempt < LEGACY_INBOUND_BOOTSTRAP_ATTEMPTS; attempt += 1) {
|
|
427
|
+
const todo = await loadExampleTodoSnapshot(em, scope, todoId)
|
|
428
|
+
if (todo) return todo
|
|
429
|
+
if (attempt < LEGACY_INBOUND_BOOTSTRAP_ATTEMPTS - 1) {
|
|
430
|
+
await sleep(LEGACY_INBOUND_BOOTSTRAP_DELAY_MS)
|
|
431
|
+
em.clear()
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null
|
|
435
|
+
}
|
|
436
|
+
|
|
397
437
|
async function ensureLegacyExampleMapping(
|
|
398
438
|
em: EntityManager,
|
|
399
439
|
scope: ExampleCustomersSyncScope,
|
|
@@ -416,7 +456,7 @@ async function ensureLegacyExampleMapping(
|
|
|
416
456
|
...scope,
|
|
417
457
|
interactionId,
|
|
418
458
|
todoId: legacyLink.todoId,
|
|
419
|
-
sourceUpdatedAt: legacyLink.createdAt
|
|
459
|
+
sourceUpdatedAt: parseDateOrNull(legacyLink.createdAt),
|
|
420
460
|
})
|
|
421
461
|
}
|
|
422
462
|
|
|
@@ -755,7 +795,7 @@ async function loadLegacyExampleTodoLinks(
|
|
|
755
795
|
const next = rows.length > limit ? pageRows[pageRows.length - 1] : null
|
|
756
796
|
return {
|
|
757
797
|
rows: pageRows,
|
|
758
|
-
...(next ? { nextCursor: encodeCursor({ createdAt: next.createdAt.toISOString(), id: next.id }) } : {}),
|
|
798
|
+
...(next ? { nextCursor: encodeCursor({ createdAt: toIsoString(next.createdAt) ?? new Date(0).toISOString(), id: next.id }) } : {}),
|
|
759
799
|
}
|
|
760
800
|
}
|
|
761
801
|
|
|
@@ -781,14 +821,14 @@ async function ensureCanonicalInteractionForLegacyLink(
|
|
|
781
821
|
return { interactionId: existing.id, created: false }
|
|
782
822
|
}
|
|
783
823
|
|
|
784
|
-
const todo = await
|
|
824
|
+
const todo = await waitForExampleTodoSnapshot(em, scope, link.todoId)
|
|
785
825
|
if (!todo) return null
|
|
786
826
|
|
|
787
827
|
const patch = buildInteractionUpdateFromExampleTodo({
|
|
788
828
|
title: todo.title,
|
|
789
829
|
isDone: todo.isDone,
|
|
790
830
|
customValues: todo.customValues,
|
|
791
|
-
occurredAt: todo.isDone ? (todo.updatedAt ?? link.createdAt) : null,
|
|
831
|
+
occurredAt: todo.isDone ? parseDateOrNull(todo.updatedAt ?? link.createdAt) : null,
|
|
792
832
|
})
|
|
793
833
|
|
|
794
834
|
const commandBus = container.resolve('commandBus') as CommandBus
|
|
@@ -801,6 +841,8 @@ async function ensureCanonicalInteractionForLegacyLink(
|
|
|
801
841
|
const result = await commandBus.execute<Record<string, unknown>, { interactionId: string }>('customers.interactions.create', {
|
|
802
842
|
input: {
|
|
803
843
|
id: link.todoId,
|
|
844
|
+
tenantId: scope.tenantId,
|
|
845
|
+
organizationId: scope.organizationId,
|
|
804
846
|
entityId: link.entityId,
|
|
805
847
|
interactionType: CUSTOMER_INTERACTION_TASK_TYPE,
|
|
806
848
|
title: patch.title,
|
|
@@ -840,16 +882,16 @@ async function ensureMappingForLegacyExampleTodo(
|
|
|
840
882
|
todoId: string,
|
|
841
883
|
): Promise<ExampleCustomerInteractionMapping | null> {
|
|
842
884
|
const em = (container.resolve('em') as EntityManager).fork()
|
|
843
|
-
const legacyLink = await
|
|
885
|
+
const legacyLink = await waitForLegacyExampleTodoLinkRow(em, scope, todoId)
|
|
844
886
|
if (!legacyLink) return null
|
|
845
887
|
const canonical = await ensureCanonicalInteractionForLegacyLink(container, scope, legacyLink)
|
|
846
888
|
if (!canonical) return null
|
|
847
|
-
const todo = await
|
|
889
|
+
const todo = await waitForExampleTodoSnapshot(em, scope, todoId)
|
|
848
890
|
return await updateMappingAfterSync(em, {
|
|
849
891
|
...scope,
|
|
850
892
|
interactionId: canonical.interactionId,
|
|
851
893
|
todoId,
|
|
852
|
-
sourceUpdatedAt: todo?.updatedAt ?? legacyLink.createdAt,
|
|
894
|
+
sourceUpdatedAt: parseDateOrNull(todo?.updatedAt ?? legacyLink.createdAt),
|
|
853
895
|
})
|
|
854
896
|
}
|
|
855
897
|
|
|
@@ -888,7 +930,7 @@ export async function reconcileLegacyExampleTodoLinks(
|
|
|
888
930
|
...scope,
|
|
889
931
|
interactionId: canonical.interactionId,
|
|
890
932
|
todoId: row.todoId,
|
|
891
|
-
sourceUpdatedAt: todo?.updatedAt ?? row.createdAt,
|
|
933
|
+
sourceUpdatedAt: parseDateOrNull(todo?.updatedAt ?? row.createdAt),
|
|
892
934
|
})
|
|
893
935
|
items.push({
|
|
894
936
|
linkId: row.id,
|
|
File without changes
|