lucy-cli 0.10.1 → 1.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/README.md CHANGED
@@ -68,6 +68,9 @@ Lucy-CLI is designed to streamline the setup and management of TypeScript within
68
68
  7. **Add git version during production build**
69
69
  - Lucy-CLI can add the git version to the production build.
70
70
  - It adds the git version to the `public/constant/env.ts` file in the public folder under the key gitTag.
71
+ 7. **Use velo-sync to synchronize local collection with wix**
72
+ - Lucy-CLI can synchronize the local collection with the wix collection.
73
+ - More information can be found in the [velo-sync](https://www.npmjs.com/package/velo-sync) documentation.
71
74
 
72
75
  ## Commands & Options
73
76
 
@@ -79,7 +82,7 @@ Lucy-CLI comes with a range of commands and options to help manage your Wix Velo
79
82
  - **`dev`**: Starts the development environment, including setting up any required services for local development.
80
83
  - **`build-prod`**: Builds the project in production mode, optimizing files for deployment.
81
84
  - **`prepare`**: Re-runs initialization commands, useful for setting up a pre-configured environment.
82
- - **`sync`**: Synchronizes the database and any dependencies or configurations necessary for the project.
85
+ - **`velo-sync`**: Synchronizes the collections.
83
86
  - **`install`**: Installs all Wix npm packages listed in the `wixpkgs.json` file in the project directory.
84
87
  - **`fix`**: Runs a fix command to resolve common issues in development or production settings.
85
88
  - **🦮 `docs`**: Generates documentation for the project.
@@ -129,7 +132,7 @@ Commands:
129
132
  🦮 dev : Starts the development environment. This includes setting up any required services for local development.
130
133
  🦮 build-prod : Builds the project in production mode, optimizing files for deployment.
131
134
  🦮 prepare : Prepares the project by re-running initialization commands. Useful for setting up a pre-configured environment.
132
- 🦮 sync : Synchronizes the database and any dependencies or configurations necessary for the project.
135
+ 🦮 velo-sync : Synchronizes the collections.
133
136
  🦮 install : Installs all Wix npm packages listed in the 'wixpkgs.json' file in the project directory.
134
137
  🦮 fix : Runs a fix command to resolve common issues in development or production settings.
135
138
  🦮 docs : Generates documentation for the project.
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'fs';
2
- import glob from 'glob';
2
+ import { glob } from 'glob';
3
3
  import * as path from 'path';
4
4
  import gulp from 'gulp';
5
5
  import ts from 'gulp-typescript';
@@ -23,52 +23,21 @@ function extractMatchFromFile(filePath, pattern) {
23
23
  });
24
24
  });
25
25
  }
26
- /**
27
- * Reads files in a folder
28
- * @param {string} folderPath Folder path
29
- * @param {string} pattern Pattern to match
30
- * @param {string} globPattern Glob pattern
31
- */
32
- function readFilesInFolder(folderPath, pattern, globPattern) {
33
- return new Promise((resolve, reject) => {
34
- glob(path.join(folderPath, globPattern), (err, files) => {
35
- if (err) {
36
- reject(err);
37
- return;
38
- }
39
- const filenameList = [];
40
- /**
41
- * Traverse files
42
- * @param {number} index Index
43
- */
44
- function traverseFiles(index) {
45
- if (index === files.length) {
46
- resolve(filenameList);
47
- return;
48
- }
49
- const file = files[index];
50
- if (pattern) {
51
- if (!file)
52
- return;
53
- extractMatchFromFile(file, pattern)
54
- .then((capturedGroup) => {
55
- if (capturedGroup) {
56
- filenameList.push(capturedGroup);
57
- }
58
- traverseFiles(index + 1);
59
- })
60
- .catch(reject);
61
- }
62
- if (!pattern) {
63
- if (!file)
64
- return;
65
- filenameList.push(path.basename(file));
66
- traverseFiles(index + 1);
67
- }
26
+ async function readFilesInFolder(folderPath, pattern, globPattern) {
27
+ const files = await glob(path.join(folderPath, globPattern));
28
+ const filenameList = [];
29
+ for (const file of files) {
30
+ if (pattern) {
31
+ const capturedGroup = await extractMatchFromFile(file, pattern);
32
+ if (capturedGroup) {
33
+ filenameList.push(capturedGroup);
68
34
  }
69
- traverseFiles(0);
70
- });
71
- });
35
+ }
36
+ else {
37
+ filenameList.push(path.basename(file));
38
+ }
39
+ }
40
+ return filenameList;
72
41
  }
73
42
  export async function checkPages(fail, force) {
74
43
  console.log("🐕" + green.underline.bold(' => Checking pages...'));
@@ -149,9 +149,9 @@ export function updateWixTypes(options) {
149
149
  }
150
150
  export function addTypes(options, done) {
151
151
  const { replaceOptions } = options;
152
- const processPages = gulp.src(['./.wix/types/wix-code-types/dist/types/page/$w.d.ts'])
153
- .pipe(replace('declare namespace \\$w {', ' declare namespace $w{\nconst api: $w.Api;\n', replaceOptions))
154
- .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/page/'));
152
+ // const processPages = gulp.src(['./.wix/types/wix-code-types/dist/types/page/$w.d.ts'])
153
+ // .pipe(replace('declare namespace \\$w {', ' declare namespace $w{\nconst api: $w.Api;\n', replaceOptions))
154
+ // .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/page/'));
155
155
  const exportTypes = gulp.src(['./.wix/types/wix-code-types/dist/types/common/*.d.ts', '!./.wix/types/wix-code-types/dist/types/common/$w.d.ts'])
156
156
  .pipe(replace('interface ', 'export interface ', replaceOptions))
157
157
  .pipe(replace('enum ', 'export enum ', replaceOptions))
@@ -163,10 +163,12 @@ export function addTypes(options, done) {
163
163
  .pipe(replace('type ', 'export type ', replaceOptions))
164
164
  .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/beta/common/'));
165
165
  const processCommon = gulp.src(['./.wix/types/wix-code-types/dist/types/common/$w.d.ts'])
166
- .pipe(insert.prepend("import { FrontendAPI } from '../../../../../../typescript/public/models/frontendApi.model';\nimport '@total-typescript/ts-reset';\n"))
167
- .pipe(replace('namespace \\$w {', 'declare namespace $w{\ntype Api = FrontendAPI;\n', replaceOptions))
166
+ .pipe(insert.prepend("import '@total-typescript/ts-reset';\n"))
167
+ // .pipe(replace('namespace \\$w {', 'declare namespace $w{\ntype Api = FrontendAPI;\n', replaceOptions))
168
168
  .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/common/'));
169
- return merge(processPages, processCommon, exportTypesBeta, exportTypes)
169
+ return merge(
170
+ // processPages,
171
+ processCommon, exportTypesBeta, exportTypes)
170
172
  .on('error', function (e) {
171
173
  console.log("💩" + red.underline.bold(' => Updating WIX failed!'));
172
174
  console.log("💩" + red.underline.bold(` => Error: ${orange(e.message)}`));
package/dist/helpers.d.ts CHANGED
@@ -11,3 +11,9 @@ export declare function cleanupWatchers(): void;
11
11
  * @param {string} processPattern - The substring to match (e.g., "wix:dev" or "@wix/cli/bin/wix.cjs").
12
12
  */
13
13
  export declare function killAllProcesses(processPattern: string): void;
14
+ export interface VeloSyncConfig {
15
+ siteUrl: string;
16
+ secret: string;
17
+ }
18
+ export declare function saveConfig(config: VeloSyncConfig, file: string): Promise<void>;
19
+ export declare function readConfig(file: string): Promise<VeloSyncConfig>;
package/dist/helpers.js CHANGED
@@ -161,3 +161,10 @@ export function killAllProcesses(processPattern) {
161
161
  });
162
162
  });
163
163
  }
164
+ export async function saveConfig(config, file) {
165
+ await fs.promises.writeFile(file, JSON.stringify(config));
166
+ }
167
+ export async function readConfig(file) {
168
+ let content = await fs.promises.readFile(file, 'utf-8');
169
+ return JSON.parse(content);
170
+ }
package/dist/index.d.ts CHANGED
@@ -36,6 +36,7 @@ export type ModuleSettings = {
36
36
  settings: LucySettings;
37
37
  lockVersion: boolean;
38
38
  force: boolean;
39
+ veloConfigName: string;
39
40
  };
40
41
  export type ProjectSettings = {
41
42
  modules?: Record<string, string>;
package/dist/index.js CHANGED
@@ -78,12 +78,18 @@ async function main() {
78
78
  packageJsonPath: join(process.cwd(), 'package.json'),
79
79
  force: false,
80
80
  lockVersion: false,
81
+ veloConfigName: 'config.json'
81
82
  };
82
83
  let projectSettings = {};
83
84
  if (moduleSettings.args.includes('version') || moduleSettings.args.includes('-v')) {
84
85
  console.log("🐾" + blue.bold(` => ${projectPackageJSON.version}`));
85
86
  return;
86
87
  }
88
+ // Run velo sync
89
+ if (moduleSettings.args.includes('velo-sync')) {
90
+ await sync(moduleSettings, projectSettings);
91
+ return;
92
+ }
87
93
  if (moduleSettings.args.includes('help') || moduleSettings.args.includes('-h')) {
88
94
  console.log("🦮 " + green.underline.bold(' => Lucy CLI Help'));
89
95
  console.log("Usage: lucy-cli <command> [options]");
@@ -92,7 +98,7 @@ async function main() {
92
98
  console.log("🦮 " + magenta.bold('dev') + " : Starts the development environment. This includes setting up any required services for local development.");
93
99
  console.log("🦮 " + magenta.bold('build-prod') + " : Builds the project in production mode, optimizing files for deployment.");
94
100
  console.log("🦮 " + magenta.bold('prepare') + " : Prepares the project by installing packages & initializing git modules, configured in lucy.json");
95
- console.log("🦮 " + magenta.bold('sync') + " : Synchronizes the database (not Implemented)");
101
+ console.log("🦮 " + magenta.bold('velo-sync') + " : Synchronizes wix collections(velo-sync -h for help)");
96
102
  console.log("🦮 " + magenta.bold('install') + " : Installs all Wix npm packages listed in the 'lucy.json' file in the project directory.");
97
103
  console.log("🦮 " + magenta.bold('fix') + " : Runs a fix command to resolve common issues in development or production settings.");
98
104
  console.log("🦮 " + magenta.bold('docs') + " : Generates documentation for the project.");
@@ -215,10 +221,6 @@ async function main() {
215
221
  await installPackages(projectSettings.lucySettings.wixPackages, projectSettings.lucySettings.devPackages, moduleSettings.targetFolder, moduleSettings.lockVersion);
216
222
  return;
217
223
  }
218
- if (moduleSettings.args.includes('sync')) {
219
- sync(moduleSettings, projectSettings);
220
- return;
221
- }
222
224
  if (moduleSettings.args.includes('dev')) {
223
225
  runGulp(moduleSettings, projectSettings, 'dev');
224
226
  return;
@@ -20,6 +20,7 @@
20
20
  "typedoc-github-theme": "^0.2.0",
21
21
  "@types/react": "^19.0.0",
22
22
  "@vitest/ui": "^2.1.8",
23
+ "eslint-import-resolver-typescript": "^3.7.0",
23
24
  "@vitest/coverage-v8": "^2.1.8",
24
25
  "@typescript-eslint/eslint-plugin": "^8.15.0",
25
26
  "@typescript-eslint/parser": "^8.15.0",
package/dist/sync.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { ModuleSettings, ProjectSettings } from ".";
2
- export declare function sync(moduleSettings: ModuleSettings, projectSettings: ProjectSettings): void;
1
+ import { ModuleSettings, ProjectSettings } from "./index.js";
2
+ export declare function sync(moduleSettings: ModuleSettings, projectSettings: ProjectSettings): Promise<void>;
package/dist/sync.js CHANGED
@@ -1,5 +1,87 @@
1
- export function sync(moduleSettings, projectSettings) {
1
+ import chalk from "chalk";
2
+ import { green, orange, red } from "./index.js";
3
+ import readline from 'node:readline';
4
+ import veloAPI from 'velo-sync/dist/velo/velo-api.js';
5
+ import syncTask from 'velo-sync/dist/tasks/sync-task.js';
6
+ import { readConfig, saveConfig } from "./helpers.js";
7
+ import optimist from 'optimist';
8
+ import migrateFileCache from 'velo-sync/dist/tasks/migrate-files-cache-task.js';
9
+ function printUsage() {
10
+ console.log('Usage: ');
11
+ console.log('');
12
+ console.log('Commands:');
13
+ console.log(' init generates a config file for the import / export / sync process');
14
+ console.log(' is-alive tests the config and the connection to the site');
15
+ console.log(' sync runs the sync process');
16
+ console.log(' import runs an import process');
17
+ console.log(' migrate migrate existing nedb cache to sqlite cache (.upload-cache.db => .upload-cache.sqlite.db)');
18
+ }
19
+ function syncOrImportTask(importOnly) {
20
+ let argv = optimist
21
+ .usage(`Usage: $0 ${importOnly ? 'import' : 'sync'} -f <scv filename> -c <collection>`)
22
+ .demand('f')
23
+ .alias('f', 'filename')
24
+ .describe('f', 'csv filename to import')
25
+ .demand('c')
26
+ .describe('c', 'the name of the collection to import into')
27
+ .alias('c', 'collection')
28
+ .demand('s')
29
+ .describe('s', 'schema file describing the fields of the collection')
30
+ .alias('s', 'schema')
31
+ .describe('dry', 'dry-run that does not upload any data or files, and does not remove or update anything on the site')
32
+ .alias('dry', 'dryrun')
33
+ .parse(process.argv.slice(3));
34
+ let filename = argv.filename;
35
+ let collection = argv.collection;
36
+ let schema = argv.schema;
37
+ let dryrun = argv.dryrun;
38
+ //@ts-ignore
39
+ syncTask.default(filename, collection, schema, importOnly, dryrun);
40
+ }
41
+ export async function sync(moduleSettings, projectSettings) {
42
+ if (moduleSettings.args.includes('-h') || moduleSettings.args.includes('help'))
43
+ return printUsage();
44
+ if (moduleSettings.args.includes('init')) {
45
+ const rl = readline.createInterface({
46
+ input: process.stdin,
47
+ output: process.stdout,
48
+ terminal: true
49
+ });
50
+ async function askQuestion(query) {
51
+ return new Promise((resolve) => rl.question(query, (answer) => resolve(answer)));
52
+ }
53
+ console.log(chalk.yellow('hello to velo-sync init'));
54
+ let siteUrl = await askQuestion('what is the url of the site homepage? ');
55
+ let secret = await askQuestion('what is the velo-sync secret? ');
56
+ rl.close();
57
+ let config = { siteUrl, secret };
58
+ await saveConfig(config, moduleSettings.veloConfigName);
59
+ return console.log(chalk.green("🐕" + 'config saved!'));
60
+ }
61
+ if (moduleSettings.args.includes('is-alive')) {
62
+ try {
63
+ let config = await readConfig(moduleSettings.veloConfigName);
64
+ console.log("🐕" + green(` => checking if the API for site ${chalk.greenBright(config.siteUrl)} is alive...`));
65
+ await veloAPI.isAlive(config);
66
+ return console.log(chalk.green("🐕" + `API of site ${chalk.greenBright(config.siteUrl)} is working and alive!!!`));
67
+ }
68
+ catch (e) {
69
+ if (e instanceof Error) {
70
+ return console.log((`💩 ${red.underline.bold("=> Failed to check endpoint")} ${orange(e.message)}`));
71
+ }
72
+ }
73
+ }
2
74
  if (moduleSettings.args.includes('sync')) {
75
+ return syncOrImportTask(false);
76
+ }
77
+ if (moduleSettings.args.includes('import')) {
78
+ return syncOrImportTask(true);
79
+ }
80
+ if (moduleSettings.args.includes('export')) {
81
+ return console.log((`💩 ${red.underline.bold("=> Not implemented")}`));
82
+ }
83
+ if (moduleSettings.args.includes('migrate')) {
84
+ //@ts-ignore
85
+ migrateFileCache.default();
3
86
  }
4
- console.log('Hello sync');
5
87
  }
@@ -22,6 +22,11 @@ export default tseslint.config(
22
22
  'named-import-spacing': namedImportSpacing,
23
23
  jsdoc,
24
24
  },
25
+ settings: {
26
+ 'import/resolver': {
27
+ typescript: {}
28
+ }
29
+ },
25
30
  languageOptions: {
26
31
  parser: tseslint.parser,
27
32
  parserOptions: {
@@ -37,6 +42,12 @@ export default tseslint.config(
37
42
  },
38
43
  },
39
44
  rules: {
45
+ 'no-restricted-imports': [
46
+ 'error',
47
+ {
48
+ 'patterns': ['*/backend/*', '*/**/public/*']
49
+ }
50
+ ],
40
51
  'no-restricted-syntax': [
41
52
  'error',
42
53
  {
@@ -0,0 +1,65 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ import { batchCheckUpdateState, clearStale, getImageUploadUrl, insertItemBatch, isAlive, saveItemBatch } from 'backend/lib/http-functions/sync2';
3
+ import { WixHttpFunctionRequest } from 'wix-http-functions';
4
+
5
+ /**------------------------------------------------------------------------
6
+ ** Velo-Sync endpoints
7
+ * This function are for the velo-sync functionality
8
+ * https://www.npmjs.com/package/velo-sync
9
+ *------------------------------------------------------------------------**/
10
+ /**
11
+ * Handle the isAlive request
12
+ * @param request The request object
13
+ * @returns The response object
14
+ */
15
+ export async function post_isAlive(request: WixHttpFunctionRequest) {
16
+ return await isAlive(request);
17
+ }
18
+
19
+ /**
20
+ * Handle the insertItemBatch request
21
+ * @param request The request object
22
+ * @returns The response object
23
+ */
24
+ export async function post_insertItemBatch(request: WixHttpFunctionRequest) {
25
+
26
+ return await insertItemBatch(request);
27
+ }
28
+
29
+ /**
30
+ * Handle the saveItemBatch request
31
+ * @param request The request object
32
+ * @returns The response object
33
+ */
34
+ export async function post_saveItemBatch(request: WixHttpFunctionRequest) {
35
+ return await saveItemBatch(request);
36
+ }
37
+
38
+ /**
39
+ * Handle the clearStale request
40
+ * @param request The request object
41
+ * @returns The response object
42
+ */
43
+ export async function post_clearStale(request: WixHttpFunctionRequest) {
44
+ return await clearStale(request);
45
+ }
46
+
47
+ /**
48
+ * Handle the batchCheckUpdateState request
49
+ * @param request The request object
50
+ * @returns The response object
51
+ */
52
+ export async function post_batchCheckUpdateState(request: WixHttpFunctionRequest) {
53
+ return await batchCheckUpdateState(request);
54
+ }
55
+
56
+ /**
57
+ * Handle the getImageUploadUrl request
58
+ * @param request The request object
59
+ * @returns The response object
60
+ */
61
+ export async function post_getImageUploadUrl(request: WixHttpFunctionRequest) {
62
+ return await getImageUploadUrl(request);
63
+ }
64
+ /*--------------- END OF SECTION --------------*/
65
+
@@ -0,0 +1,270 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
4
+ /* eslint-disable @typescript-eslint/naming-convention */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
8
+
9
+ import crypto from 'crypto';
10
+ import wixData from 'wix-data';
11
+ import { forbidden, ok, serverError, WixHttpFunctionRequest, WixHttpFunctionResponse } from 'wix-http-functions';
12
+ import { mediaManager } from 'wix-media-backend';
13
+ import wixSecretsBackend from 'wix-secrets-backend';
14
+
15
+ /**----------------------------------------------
16
+ * * INFO
17
+ * URL to call this HTTP function from your published site looks like:
18
+ * Premium site - https://mysite.com/_functions/example/multiply?leftOperand=3&rightOperand=4
19
+ * Free site - https://username.wixsite.com/mysite/_functions/example/multiply?leftOperand=3&rightOperand=4
20
+ *
21
+ * URL to test this HTTP function from your saved site looks like:
22
+ * Premium site - https://mysite.com/_functions-dev/example/multiply?leftOperand=3&rightOperand=4
23
+ * Free site - https://username.wixsite.com/mysite/_functions-dev/example/multiply?leftOperand=3&rightOperand=4
24
+ *---------------------------------------------**/
25
+
26
+ //! WARNING: The following code is taken out of velo-sync package.
27
+ //! WARNING: This serves as example and is not tested.
28
+
29
+ class ForbiddenError extends Error {
30
+ constructor(message: string) {
31
+ super(message);
32
+ this.type = FORBIDDEN;
33
+ }
34
+ type: string;
35
+ }
36
+
37
+ const FORBIDDEN = 'forbidden';
38
+
39
+ /**
40
+ * Validates the request and parses the payload
41
+ * @param request - the request object
42
+ * @returns the parsed payload
43
+ */
44
+ export async function validateAndParseRequest(request: WixHttpFunctionRequest) {
45
+ const payload = await request.body.text();
46
+ const payloadJson = JSON.parse(payload, dateReviver) as any;
47
+ const secret = await wixSecretsBackend.getSecret('velo-sync');
48
+ const hmac = crypto.createHmac('sha256', secret);
49
+ hmac.update(JSON.stringify(payloadJson.data, dateReplacer));
50
+ const digest = hmac.digest('hex');
51
+ if (digest !== payloadJson.signature){
52
+ const forbiddenError = new ForbiddenError('invalid signature check');
53
+ forbiddenError.type = FORBIDDEN;
54
+ throw forbiddenError;
55
+ }
56
+
57
+ return payloadJson.data;
58
+ }
59
+
60
+ /**
61
+ * Logs the request and handles the response
62
+ * @param name - the name of the request
63
+ * @param handler - the handler function
64
+ * @returns the response object
65
+ */
66
+ export async function logRequest(name:string, handler: () => Promise<any>) {
67
+ console.log(name, 'start');
68
+ const start = new Date().getTime();
69
+ try {
70
+ const response = await handler();
71
+ const now = new Date().getTime();
72
+ console.log(name, 'completed ok, time:', now - start);
73
+
74
+ return ok({ body: response });
75
+ }
76
+ catch (e: any){
77
+ const now = new Date().getTime();
78
+ if (e.type === FORBIDDEN){
79
+ console.log(name, 'forbidden:', e.message, ', time:', now - start);
80
+
81
+ return forbidden({ body: e.message });
82
+ }
83
+ else {
84
+ console.log(name, 'failed with error:', e.message, ', time:', now - start);
85
+
86
+ return serverError({ body: e.message });
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Checks if the service is alive
93
+ * @param request - the request object
94
+ * @returns the response object
95
+ */
96
+ export async function isAlive(request: WixHttpFunctionRequest): Promise<WixHttpFunctionResponse> {
97
+ return await logRequest('isAlive', async () => {
98
+ const data = await validateAndParseRequest(request);
99
+ if (data.isAlive === '?')
100
+ {return 'ok';}
101
+ else
102
+ {throw new Error('protocol error - the isAlive API expects isAlive member in the data payload');}
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Inserts a batch of items
108
+ * @param request - the request object
109
+ * @returns the response object
110
+ */
111
+ export async function insertItemBatch(request: WixHttpFunctionRequest): Promise<WixHttpFunctionResponse> {
112
+ return await logRequest('insertItemBatch', async () => {
113
+ const data = await validateAndParseRequest(request);
114
+ const itemsToInsert = data.items;
115
+ const collection = data.collection;
116
+
117
+ return await wixData.bulkInsert(collection, itemsToInsert, { suppressAuth: true });
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Saves a batch of items
123
+ * @param request - the request object
124
+ * @returns the response object
125
+ */
126
+ export async function saveItemBatch(request: WixHttpFunctionRequest): Promise<WixHttpFunctionResponse> {
127
+ return await logRequest('saveItemBatch', async () => {
128
+ const data = await validateAndParseRequest(request);
129
+ const items = data.items;
130
+ const collection = data.collection;
131
+
132
+ return await wixData.bulkSave(collection, items, { suppressAuth: true });
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Clears stale items
138
+ * @param request - the request object
139
+ * @returns the response object
140
+ */
141
+ export async function clearStale(request: WixHttpFunctionRequest): Promise<WixHttpFunctionResponse> {
142
+ return await logRequest('clearStale', async () => {
143
+ const data = await validateAndParseRequest(request);
144
+ const collection = data.collection;
145
+
146
+ const date = new Date();
147
+ date.setDate(date.getDate() - 3);
148
+
149
+ const res = await wixData.query(collection)
150
+ .lt('_updatedDate', date)
151
+ .find({ suppressAuth: true });
152
+ console.log(`clearStale - found ${res.totalCount} items to remove, current page ${res.length}`);
153
+ const itemsToDelete = res.items;
154
+ const ids = itemsToDelete.map((_: any )=> _._id);
155
+ const removeResult = await wixData.bulkRemove(collection, ids, { suppressAuth: true });
156
+
157
+ return { itemsRemoved: removeResult.removed, staleItems: res.totalCount - removeResult.removed, errors: removeResult.errors };
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Batch checks the update state of items
163
+ * @param request - the request object
164
+ * @returns the response object
165
+ */
166
+ export async function batchCheckUpdateState(request: WixHttpFunctionRequest): Promise<WixHttpFunctionResponse> {
167
+ return await logRequest('batchCheckUpdateState', async () => {
168
+ const data = await validateAndParseRequest(request);
169
+
170
+ const collection = data.collection;
171
+ const items = data.items;
172
+ const dryrun = data.dryrun;
173
+
174
+ const queries = items.map((item: any) => wixData.query(collection).eq('_id', item._id));
175
+
176
+ const query = queries.reduce((accuQuery: any, query: any) => (accuQuery)?accuQuery.or(query): query);
177
+ const result:any[] = [];
178
+ const itemsToUpdate: any[] = [];
179
+ const res = await query.find({ suppressAuth: true });
180
+ items.forEach((item: any) => {
181
+ const foundItem = res.items.find((_: any) => _._id === item._id);
182
+ if (foundItem && foundItem._hash === item._hash){
183
+ itemsToUpdate.push(foundItem);
184
+ result.push({ status: 'ok', _id: item._id });
185
+ }
186
+ else if (foundItem){
187
+ result.push({ status: 'need-update', _id: item._id });
188
+ }
189
+ else {
190
+ result.push({ status: 'not-found', _id: item._id });
191
+ }
192
+ });
193
+ if (!dryrun)
194
+ {await wixData.bulkUpdate(collection, itemsToUpdate, { suppressAuth: true });}
195
+
196
+ return JSON.stringify(result);
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Gets an image upload URL
202
+ * @param request - the request object
203
+ * @returns the response object
204
+ */
205
+ export async function getImageUploadUrl(request: WixHttpFunctionRequest): Promise<WixHttpFunctionResponse> {
206
+ return await logRequest('getImageUploadUrl', async () => {
207
+ const data = await validateAndParseRequest(request);
208
+
209
+ const mimeType = data.mimeTypes;
210
+ const _id = data._id;
211
+ const fieldName = data.fieldName;
212
+ const collection = data.collection;
213
+ const mediaType = data.mediaType;
214
+
215
+ const uploadUrlObj = await mediaManager.getUploadUrl('/synced-images',
216
+ {
217
+ 'mediaOptions': {
218
+ mimeType,
219
+ mediaType
220
+ },
221
+ 'metadataOptions': {
222
+ 'isPrivate': false,
223
+ 'isVisitorUpload': false,
224
+ 'context': {
225
+ _id,
226
+ fieldName,
227
+ collection
228
+ }
229
+ }
230
+ });
231
+
232
+ return uploadUrlObj;
233
+ });
234
+ }
235
+
236
+ const dateRegex = /^Date\((\d+)\)$/;
237
+ /**
238
+ *
239
+ * @param key
240
+ * @param value
241
+ */
242
+
243
+ /**
244
+ * Reviver function for JSON.parse that converts string representations of Date objects to Date objects
245
+ * @param key The key of the value
246
+ * @param value The value to convert
247
+ * @returns The converted Date object
248
+ */
249
+ export function dateReviver(key: string, value: string) {
250
+ const match = dateRegex.exec(value);
251
+ if (match){
252
+ return new Date(Number(match[1]));
253
+ }
254
+
255
+ return value;
256
+ }
257
+
258
+ /**
259
+ * Replacer function for JSON.stringify that converts Date objects to a string representation
260
+ * @param key The key of the value
261
+ * @param value The value to convert
262
+ * @returns The converted value
263
+ */
264
+ export function dateReplacer(this: any, key: string, value: string) {
265
+ const v = this[key];
266
+ if (v instanceof Date)
267
+ {return 'Date('+v.getTime()+')';}
268
+ else
269
+ {return value;}
270
+ }
@@ -5,6 +5,7 @@
5
5
  "compilerOptions": {
6
6
  "outDir": "../src",
7
7
  "rootDir": ".",
8
+ "baseUrl": "./",
8
9
  "target": "ES2020",
9
10
  "module": "ES2020",
10
11
  "moduleResolution": "Node",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "lucy-cli",
4
- "version": "0.10.1",
4
+ "version": "1.0.1",
5
5
  "description": "Lucy Framework for WIX Studio Editor",
6
6
  "main": ".dist/index.js",
7
7
  "scripts": {
@@ -61,6 +61,7 @@
61
61
  "jest": "^29.7.0",
62
62
  "merge-stream": "^2.0.0",
63
63
  "merge2": "^1.4.1",
64
+ "optimist": "^0.6.1",
64
65
  "prettier": "^3.0.3",
65
66
  "sass": "^1.65.1",
66
67
  "simple-git": "^3.20.0",
@@ -84,6 +85,7 @@
84
85
  "@types/jest": "^29.5.3",
85
86
  "@types/merge-stream": "2.0.0",
86
87
  "@types/merge2": "^1.4.4",
88
+ "@types/optimist": "^0.0.33",
87
89
  "@typescript-eslint/eslint-plugin": "8.14.0",
88
90
  "@typescript-eslint/parser": "8.14.0",
89
91
  "@typescript-eslint/utils": "8.14.0",
@@ -92,6 +94,8 @@
92
94
  "eslint-plugin-jsdoc": "50.5.0",
93
95
  "eslint-plugin-named-import-spacing": "^1.0.3",
94
96
  "eslint-plugin-simple-import-sort": "12.1.1",
97
+ "i": "^0.3.7",
98
+ "npm": "^11.0.0",
95
99
  "ts-node": "^10.9.1"
96
100
  }
97
101
  }
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'fs';
2
- import glob from 'glob';
2
+ import { glob } from 'glob';
3
3
  import * as path from 'path';
4
4
  import gulp from 'gulp';
5
5
  import ts from 'gulp-typescript';
@@ -27,54 +27,23 @@ function extractMatchFromFile(filePath: string, pattern: string) {
27
27
  });
28
28
  }
29
29
 
30
- /**
31
- * Reads files in a folder
32
- * @param {string} folderPath Folder path
33
- * @param {string} pattern Pattern to match
34
- * @param {string} globPattern Glob pattern
35
- */
36
- function readFilesInFolder(folderPath: string, pattern: string | null, globPattern: string,) {
37
- return new Promise((resolve, reject) => {
38
- glob(path.join(folderPath, globPattern), (err: unknown, files: string[]) => {
39
- if (err){
40
- reject(err);
30
+ async function readFilesInFolder(folderPath: string, pattern: string | null, globPattern: string): Promise<Object[]> {
31
+ const files = await glob(path.join(folderPath, globPattern));
32
+ const filenameList: Object[] = [];
41
33
 
42
- return;
43
- }
44
- const filenameList: Object[] = [];
45
- /**
46
- * Traverse files
47
- * @param {number} index Index
48
- */
49
- function traverseFiles(index: number) {
50
- if (index === files.length){
51
- resolve(filenameList);
52
-
53
- return;
54
- }
55
- const file = files[index];
56
- if(pattern){
57
- if(!file) return
58
- extractMatchFromFile(file, pattern)
59
- .then((capturedGroup) => {
60
- if (capturedGroup){
61
- filenameList.push(capturedGroup);
62
- }
63
- traverseFiles(index + 1);
64
- })
65
- .catch(reject);
66
- }
67
- if(!pattern){
68
- if(!file) return
69
- filenameList.push(path.basename(file));
70
- traverseFiles(index + 1);
71
- }
72
- }
73
- traverseFiles(0);
74
- });
75
- });
76
- }
34
+ for (const file of files) {
35
+ if (pattern) {
36
+ const capturedGroup = await extractMatchFromFile(file, pattern);
37
+ if (capturedGroup) {
38
+ filenameList.push(capturedGroup);
39
+ }
40
+ } else {
41
+ filenameList.push(path.basename(file));
42
+ }
43
+ }
77
44
 
45
+ return filenameList;
46
+ }
78
47
  export async function checkPages(fail: boolean, force: boolean) {
79
48
  console.log("🐕" + green.underline.bold(' => Checking pages...'));
80
49
  return new Promise<void>(async (resolve, reject) => {
package/src/gulp/types.ts CHANGED
@@ -159,9 +159,9 @@ export function updateWixTypes(options: TaskOptions) {
159
159
 
160
160
  export function addTypes(options: TaskOptions, done: gulp.TaskFunctionCallback): NodeJS.ReadWriteStream {
161
161
  const { replaceOptions } = options;
162
- const processPages = gulp.src(['./.wix/types/wix-code-types/dist/types/page/$w.d.ts'])
163
- .pipe(replace('declare namespace \\$w {', ' declare namespace $w{\nconst api: $w.Api;\n', replaceOptions))
164
- .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/page/'));
162
+ // const processPages = gulp.src(['./.wix/types/wix-code-types/dist/types/page/$w.d.ts'])
163
+ // .pipe(replace('declare namespace \\$w {', ' declare namespace $w{\nconst api: $w.Api;\n', replaceOptions))
164
+ // .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/page/'));
165
165
 
166
166
  const exportTypes = gulp.src(['./.wix/types/wix-code-types/dist/types/common/*.d.ts', '!./.wix/types/wix-code-types/dist/types/common/$w.d.ts'])
167
167
  .pipe(replace('interface ', 'export interface ', replaceOptions))
@@ -176,12 +176,12 @@ export function addTypes(options: TaskOptions, done: gulp.TaskFunctionCallback):
176
176
  .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/beta/common/'));
177
177
 
178
178
  const processCommon = gulp.src(['./.wix/types/wix-code-types/dist/types/common/$w.d.ts'])
179
- .pipe(insert.prepend("import { FrontendAPI } from '../../../../../../typescript/public/models/frontendApi.model';\nimport '@total-typescript/ts-reset';\n"))
180
- .pipe(replace('namespace \\$w {', 'declare namespace $w{\ntype Api = FrontendAPI;\n', replaceOptions))
179
+ .pipe(insert.prepend("import '@total-typescript/ts-reset';\n"))
180
+ // .pipe(replace('namespace \\$w {', 'declare namespace $w{\ntype Api = FrontendAPI;\n', replaceOptions))
181
181
  .pipe(gulp.dest('./.wix/types/wix-code-types/dist/types/common/'));
182
182
 
183
183
  return merge(
184
- processPages,
184
+ // processPages,
185
185
  processCommon,
186
186
  exportTypesBeta,
187
187
  exportTypes,
package/src/helpers.ts CHANGED
@@ -175,4 +175,16 @@ export function killAllProcesses(processPattern: string) {
175
175
  }
176
176
  });
177
177
  });
178
+ }
179
+
180
+ export interface VeloSyncConfig {
181
+ siteUrl: string;
182
+ secret: string;
183
+ }
184
+ export async function saveConfig(config:VeloSyncConfig, file: string) {
185
+ await fs.promises.writeFile(file, JSON.stringify(config));
186
+ }
187
+ export async function readConfig(file: string): Promise<VeloSyncConfig> {
188
+ let content = await fs.promises.readFile(file, 'utf-8');
189
+ return JSON.parse(content);
178
190
  }
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ export type ModuleSettings = {
53
53
  settings: LucySettings;
54
54
  lockVersion: boolean;
55
55
  force: boolean;
56
+ veloConfigName: string;
56
57
  }
57
58
 
58
59
  export type ProjectSettings = {
@@ -136,6 +137,7 @@ async function main(): Promise<void> {
136
137
  packageJsonPath: join(process.cwd(), 'package.json'),
137
138
  force: false,
138
139
  lockVersion: false,
140
+ veloConfigName: 'config.json'
139
141
  }
140
142
 
141
143
  let projectSettings: ProjectSettings = {};
@@ -145,6 +147,12 @@ async function main(): Promise<void> {
145
147
 
146
148
  return;
147
149
  }
150
+ // Run velo sync
151
+ if(moduleSettings.args.includes('velo-sync')){
152
+ await sync(moduleSettings, projectSettings);
153
+
154
+ return;
155
+ }
148
156
 
149
157
  if(moduleSettings.args.includes('help') || moduleSettings.args.includes('-h')){
150
158
  console.log("🦮 " + green.underline.bold(' => Lucy CLI Help'));
@@ -154,7 +162,7 @@ async function main(): Promise<void> {
154
162
  console.log("🦮 " + magenta.bold('dev') + " : Starts the development environment. This includes setting up any required services for local development.");
155
163
  console.log("🦮 " + magenta.bold('build-prod') + " : Builds the project in production mode, optimizing files for deployment.");
156
164
  console.log("🦮 " + magenta.bold('prepare') + " : Prepares the project by installing packages & initializing git modules, configured in lucy.json");
157
- console.log("🦮 " + magenta.bold('sync') + " : Synchronizes the database (not Implemented)");
165
+ console.log("🦮 " + magenta.bold('velo-sync') + " : Synchronizes wix collections(velo-sync -h for help)");
158
166
  console.log("🦮 " + magenta.bold('install') + " : Installs all Wix npm packages listed in the 'lucy.json' file in the project directory.");
159
167
  console.log("🦮 " + magenta.bold('fix') + " : Runs a fix command to resolve common issues in development or production settings.");
160
168
  console.log("🦮 " + magenta.bold('docs') + " : Generates documentation for the project.");
@@ -223,6 +231,7 @@ async function main(): Promise<void> {
223
231
 
224
232
  console.log("🐕" + magenta.underline(' => Lucy CLI => RUNNING: ' + orange('Press Ctrl+C to stop.')));
225
233
  // INFO: Run commands
234
+
226
235
  if(moduleSettings.args.includes('init')){
227
236
  if(projectSettings.lucySettings?.initialized && !moduleSettings.force) {
228
237
  console.log((`💩 ${red.underline.bold("=> This project is already initialized =>")} ${orange(moduleSettings.targetFolder)}`));
@@ -289,12 +298,7 @@ async function main(): Promise<void> {
289
298
 
290
299
  return;
291
300
  }
292
-
293
- if(moduleSettings.args.includes('sync')){
294
- sync(moduleSettings, projectSettings);
295
301
 
296
- return;
297
- }
298
302
 
299
303
  if(moduleSettings.args.includes('dev')){
300
304
  runGulp(moduleSettings, projectSettings, 'dev');
package/src/settings.json CHANGED
@@ -20,6 +20,7 @@
20
20
  "typedoc-github-theme": "^0.2.0",
21
21
  "@types/react": "^19.0.0",
22
22
  "@vitest/ui": "^2.1.8",
23
+ "eslint-import-resolver-typescript": "^3.7.0",
23
24
  "@vitest/coverage-v8": "^2.1.8",
24
25
  "@typescript-eslint/eslint-plugin": "^8.15.0",
25
26
  "@typescript-eslint/parser": "^8.15.0",
package/src/sync.ts CHANGED
@@ -1,10 +1,97 @@
1
- import { modifierNames } from "chalk";
2
- import { ModuleSettings, ProjectSettings } from ".";
1
+ import chalk, { modifierNames } from "chalk";
2
+ import { blue, green, ModuleSettings, orange, ProjectSettings, red } from "./index.js";
3
+ import readline from 'node:readline';
4
+ import {createDataSync, LoggingStatistics, LoggerRejectsReporter} from 'velo-sync';
5
+ import veloAPI from 'velo-sync/dist/velo/velo-api.js';
6
+ import syncTask from 'velo-sync/dist/tasks/sync-task.js';
7
+ import { readConfig, saveConfig, VeloSyncConfig } from "./helpers.js";
8
+ import optimist from 'optimist';
9
+ import migrateFileCache from 'velo-sync/dist/tasks/migrate-files-cache-task.js';
10
+ function printUsage() {
11
+ console.log('Usage: ');
12
+ console.log('');
13
+ console.log('Commands:');
14
+ console.log(' init generates a config file for the import / export / sync process');
15
+ console.log(' is-alive tests the config and the connection to the site');
16
+ console.log(' sync runs the sync process');
17
+ console.log(' import runs an import process');
18
+ console.log(' migrate migrate existing nedb cache to sqlite cache (.upload-cache.db => .upload-cache.sqlite.db)');
19
+ }
3
20
 
4
- export function sync(moduleSettings: ModuleSettings, projectSettings: ProjectSettings) {
21
+ function syncOrImportTask(importOnly: boolean) {
22
+ let argv = optimist
23
+ .usage(`Usage: $0 ${importOnly ? 'import' : 'sync'} -f <scv filename> -c <collection>`)
24
+ .demand('f')
25
+ .alias('f', 'filename')
26
+ .describe('f', 'csv filename to import')
27
+ .demand('c')
28
+ .describe('c', 'the name of the collection to import into')
29
+ .alias('c', 'collection')
30
+ .demand('s')
31
+ .describe('s', 'schema file describing the fields of the collection')
32
+ .alias('s', 'schema')
33
+ .describe('dry', 'dry-run that does not upload any data or files, and does not remove or update anything on the site')
34
+ .alias('dry', 'dryrun')
35
+ .parse(process.argv.slice(3));
36
+ let filename = argv.filename;
37
+ let collection = argv.collection;
38
+ let schema = argv.schema;
39
+ let dryrun = argv.dryrun;
40
+ //@ts-ignore
41
+ syncTask.default(filename, collection, schema, importOnly, dryrun);
42
+ }
5
43
 
6
- if(moduleSettings.args.includes('sync')){
7
-
44
+ export async function sync(moduleSettings: ModuleSettings, projectSettings: ProjectSettings) {
45
+
46
+ if(moduleSettings.args.includes('-h') || moduleSettings.args.includes('help')) return printUsage();
47
+ if(moduleSettings.args.includes('init')) {
48
+ const rl = readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ terminal: true
52
+ });
53
+
54
+ async function askQuestion(query: string):Promise<string> {
55
+ return new Promise((resolve) => rl.question(query, (answer) => resolve(answer)));
56
+ }
57
+
58
+ console.log(chalk.yellow('hello to velo-sync init'));
59
+ let siteUrl = await askQuestion('what is the url of the site homepage? ');
60
+ let secret = await askQuestion('what is the velo-sync secret? ');
61
+ rl.close();
62
+ let config: VeloSyncConfig = { siteUrl, secret };
63
+ await saveConfig(config, moduleSettings.veloConfigName);
64
+ return console.log(chalk.green("🐕" + 'config saved!'));
8
65
  }
9
- console.log('Hello sync');
66
+
67
+ if(moduleSettings.args.includes('is-alive')) {
68
+ try {
69
+ let config = await readConfig(moduleSettings.veloConfigName);
70
+ console.log("🐕" + green(` => checking if the API for site ${chalk.greenBright(config.siteUrl)} is alive...`));
71
+ await veloAPI.isAlive(config);
72
+ return console.log(chalk.green("🐕" + `API of site ${chalk.greenBright(config.siteUrl)} is working and alive!!!`));
73
+ }
74
+ catch (e) {
75
+ if(e instanceof Error) {
76
+ return console.log((`💩 ${red.underline.bold("=> Failed to check endpoint")} ${orange(e.message)}`));
77
+ }
78
+ }
79
+ }
80
+ if(moduleSettings.args.includes('sync')) {
81
+ return syncOrImportTask(false);
82
+ }
83
+
84
+ if(moduleSettings.args.includes('import')) {
85
+ return syncOrImportTask(true);
86
+ }
87
+
88
+ if(moduleSettings.args.includes('export')) {
89
+ return console.log((`💩 ${red.underline.bold("=> Not implemented")}`));
90
+ }
91
+
92
+ if(moduleSettings.args.includes('migrate')) {
93
+ //@ts-ignore
94
+ migrateFileCache.default();
95
+ }
96
+
10
97
  }
package/src/types.d.ts CHANGED
@@ -5,4 +5,4 @@ declare module 'gulp-string-replace';
5
5
  declare module 'gulp-wait';
6
6
  declare module 'gulp-jest';
7
7
  declare module 'gulp-flatmap';
8
- declare module 'gulp-swc';
8
+ declare module 'gulp-swc';