diff --git a/.github/workflows/code-verify.yml b/.github/workflows/code-verify.yml index e12aef4e..f6c0eda9 100644 --- a/.github/workflows/code-verify.yml +++ b/.github/workflows/code-verify.yml @@ -62,5 +62,5 @@ jobs: - name: NPM / Verify working-directory: code run: | - npm ci + npm ci --force npm run verify diff --git a/README.md b/README.md index e1b81901..9ab32c5a 100644 --- a/README.md +++ b/README.md @@ -103,24 +103,12 @@ This backend is designed to demonstrate how Weave.js can be integrated into a fu You can locally launch the backend showcase by: - Install dependencies with: `npm install` -- Create a `.env` file on the folder `/code`, and setup the necessary configuration: +- Create your local environment file from the template (from `/code`): `cp .env.example .env` +- Update `/code/.env` with the required values (including PostgreSQL and Azure variables). - ``` - HOSTNAME=0.0.0.0 - PORT=8081 - LOG_LEVEL=debug - - AZURE_WEB_PUBSUB_ENDPOINT= - AZURE_WEB_PUBSUB_KEY= - AZURE_WEB_PUBSUB_HUB_NAME=weavejs - PERSIST_FREQUENCY_SEG=10 - - AZURE_STORAGE_CONNECTION_STRING= - AZURE_STORAGE_ROOMS_CONTAINER_NAME=rooms - AZURE_STORAGE_IMAGES_CONTAINER_NAME=images - ``` - -- Run the frontend: `npm run dev` +- Start PostgreSQL with the provided compose file: `docker compose -f code/docker-compose.yml up -d db` +- Run database migrations (from `/code`): `cd code && npm run db:migrate:dev` +- Run the backend (from `/code`): `cd code && npm run dev` You'll need access to a: diff --git a/code/.env.example b/code/.env.example new file mode 100644 index 00000000..e771a749 --- /dev/null +++ b/code/.env.example @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# +# SPDX-License-Identifier: Apache-2.0 + +DEV_WEAVEJS_REPO_PATH= + +NODE_ENV=development +HOSTNAME=0.0.0.0 +PORT=8081 +DEPLOY_ENVIRONMENT=local +LOG_LEVEL=info +HTTP_LOG_LEVEL=debug + +# Development environment +AZURE_WEB_PUBSUB_ENDPOINT= +AZURE_WEB_PUBSUB_KEY= +AZURE_WEB_PUBSUB_HUB_NAME=weavejs +PERSIST_FREQUENCY_SEG=10 +CLEANUP_ROOMS_INTERVAL_SEG=60 + +AZURE_STORAGE_ACCOUNT_NAME= +AZURE_STORAGE_CONNECTION_STRING= +AZURE_STORAGE_ROOMS_CONTAINER_NAME=rooms +AZURE_STORAGE_IMAGES_CONTAINER_NAME=images +AZURE_STORAGE_VIDEOS_CONTAINER_NAME=videos +AZURE_STORAGE_GENERATED_IMAGES_CONTAINER_NAME=generated-images + +AI_SERVICES=false +AI_PASSWORD= + +AZURE_CS_ENDPOINT= +AZURE_CS_API_KEY= +AZURE_CS_TIMEOUT_SECS=240 + +APP_HOST=https://localhost:3000 +GOOGLE_GENERATIVE_AI_API_KEY= +GOOGLE_API_KEY= + +DATABASE_HOST=localhost +DATABASE_PORT=5434 +DATABASE_NAME=weavejs +DATABASE_USERNAME=weave_dbuser +DATABASE_PASSWORD=weave_password +DATABASE_SSL=false +DATABASE_CLOUD_CREDENTIALS=false + +FEATURE_WORKLOADS=true +FEATURE_THREADS=true + +LITELLM_ENDPOINT= +LITELLM_API_KEY= + +DISABLE_ROOMS_CLEANUP=false + +# Local +BETTER_AUTH_SECRET= +BETTER_AUTH_URL= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +PRINT_MEMORY_USAGE=false +PRINT_MEMORY_USAGE_INTERVAL=500 diff --git a/code/.gitignore b/code/.gitignore index 7f5f14d1..d89c37e5 100644 --- a/code/.gitignore +++ b/code/.gitignore @@ -28,6 +28,7 @@ vite.config.ts* *.pem .env* +!.env.example .npmrc temp @@ -43,4 +44,4 @@ env.sh .mastra -mcp.json \ No newline at end of file +mcp.json diff --git a/code/.tool-versions b/code/.tool-versions index 1a0f7355..9d709112 100644 --- a/code/.tool-versions +++ b/code/.tool-versions @@ -1,2 +1 @@ nodejs 24.11.1 -ivm-node 24.11.1 diff --git a/code/migrations/20251118164400-feature-auth.cjs b/code/migrations/20251118164400-feature-auth.cjs new file mode 100644 index 00000000..3c06dfe5 --- /dev/null +++ b/code/migrations/20251118164400-feature-auth.cjs @@ -0,0 +1,90 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + await queryInterface.sequelize.query('CREATE SCHEMA IF NOT EXISTS auth;', { transaction }); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS auth."user" ( + "id" text NOT NULL PRIMARY KEY, + "name" text NOT NULL, + "email" text NOT NULL UNIQUE, + "emailVerified" boolean NOT NULL DEFAULT false, + "image" text, + "createdAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `, { transaction }); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS auth."session" ( + "id" text NOT NULL PRIMARY KEY, + "userId" text NOT NULL, + "token" text NOT NULL UNIQUE, + "expiresAt" timestamptz NOT NULL, + "ipAddress" text, + "userAgent" text, + "createdAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("userId") REFERENCES auth."user" ("id") ON DELETE CASCADE + ); + `, { transaction }); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS auth."account" ( + "id" text NOT NULL PRIMARY KEY, + "userId" text NOT NULL, + "accountId" text NOT NULL, + "providerId" text NOT NULL, + "accessToken" text, + "refreshToken" text, + "accessTokenExpiresAt" timestamptz, + "refreshTokenExpiresAt" timestamptz, + "scope" text, + "idToken" text, + "password" text, + "createdAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("userId") REFERENCES auth."user" ("id") ON DELETE CASCADE + ); + `, { transaction }); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS auth."verification" ( + "id" text NOT NULL PRIMARY KEY, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expiresAt" timestamptz NOT NULL, + "createdAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down (queryInterface) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + await queryInterface.sequelize.query('DROP TABLE IF EXISTS auth."verification";', { transaction }); + await queryInterface.sequelize.query('DROP TABLE IF EXISTS auth."account";', { transaction }); + await queryInterface.sequelize.query('DROP TABLE IF EXISTS auth."session";', { transaction }); + await queryInterface.sequelize.query('DROP TABLE IF EXISTS auth."user";', { transaction }); + await queryInterface.sequelize.query('DROP SCHEMA IF EXISTS auth;', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/code/migrations/20251118164500-feature-task.cjs b/code/migrations/20251118164500-feature-task.cjs new file mode 100644 index 00000000..14c3c5ba --- /dev/null +++ b/code/migrations/20251118164500-feature-task.cjs @@ -0,0 +1,63 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + const tableExists = await queryInterface.tableExists('weavejs_task'); + + if (tableExists) { + return; + } + + await queryInterface.createTable('weavejs_task', { + jobId: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + roomId: { + type: Sequelize.STRING, + allowNull: false, + }, + userId: { + type: Sequelize.STRING, + allowNull: false, + }, + type: { + type: Sequelize.STRING, + allowNull: false, + }, + status: { + type: Sequelize.STRING, + allowNull: false, + }, + opened: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + metadata: { + type: Sequelize.JSONB, + allowNull: true, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn('NOW'), + }, + }); + }, + + async down (queryInterface) { + const table = await queryInterface.tableExists('weavejs_task'); + + if (table) { + await queryInterface.dropTable('weavejs_task'); + } + }, +}; diff --git a/code/migrations/20251118164550-feature-legacy-sequelize-tables.cjs b/code/migrations/20251118164550-feature-legacy-sequelize-tables.cjs new file mode 100644 index 00000000..83daf5bf --- /dev/null +++ b/code/migrations/20251118164550-feature-legacy-sequelize-tables.cjs @@ -0,0 +1,230 @@ +'use strict'; + +const timestampColumns = (Sequelize) => ({ + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn('NOW'), + }, +}); + +const mediaColumns = (Sequelize, idColumn) => ({ + [idColumn]: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + operation: { + type: Sequelize.STRING, + allowNull: false, + }, + status: { + type: Sequelize.STRING, + allowNull: false, + }, + mimeType: { + type: Sequelize.STRING, + allowNull: true, + }, + width: { + type: Sequelize.INTEGER, + allowNull: true, + }, + height: { + type: Sequelize.INTEGER, + allowNull: true, + }, + aspectRatio: { + type: Sequelize.FLOAT, + allowNull: true, + }, + fileName: { + type: Sequelize.STRING, + allowNull: true, + }, + jobId: { + type: Sequelize.STRING, + allowNull: true, + references: { + model: 'weavejs_task', + key: 'jobId', + }, + }, + removalJobId: { + type: Sequelize.STRING, + allowNull: true, + references: { + model: 'weavejs_task', + key: 'jobId', + }, + }, + removalStatus: { + type: Sequelize.STRING, + allowNull: true, + }, +}); + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + if (!(await queryInterface.tableExists('weavejs_connection'))) { + await queryInterface.createTable('weavejs_connection', { + connectionId: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + roomId: { + type: Sequelize.STRING, + allowNull: true, + }, + status: { + type: Sequelize.STRING, + allowNull: false, + }, + ...timestampColumns(Sequelize), + }, { transaction }); + } + + if (!(await queryInterface.tableExists('weavejs_thread'))) { + await queryInterface.createTable('weavejs_thread', { + threadId: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + userId: { + type: Sequelize.STRING, + allowNull: false, + }, + roomId: { + type: Sequelize.STRING, + allowNull: false, + }, + userMetadata: { + type: Sequelize.JSONB, + allowNull: false, + }, + x: { + type: Sequelize.FLOAT, + allowNull: false, + }, + y: { + type: Sequelize.FLOAT, + allowNull: false, + }, + status: { + type: Sequelize.STRING, + allowNull: false, + }, + content: { + type: Sequelize.TEXT, + allowNull: true, + }, + replies: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + }, + ...timestampColumns(Sequelize), + }, { transaction }); + } + + if (!(await queryInterface.tableExists('weavejs_thread_answer'))) { + await queryInterface.createTable('weavejs_thread_answer', { + answerId: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + threadId: { + type: Sequelize.STRING, + allowNull: false, + references: { + model: 'weavejs_thread', + key: 'threadId', + }, + onDelete: 'CASCADE', + }, + userId: { + type: Sequelize.STRING, + allowNull: false, + }, + userMetadata: { + type: Sequelize.JSONB, + allowNull: false, + }, + content: { + type: Sequelize.TEXT, + allowNull: true, + }, + ...timestampColumns(Sequelize), + }, { transaction }); + } + + if (!(await queryInterface.tableExists('weavejs_image'))) { + await queryInterface.createTable('weavejs_image', { + roomId: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + ...mediaColumns(Sequelize, 'imageId'), + ...timestampColumns(Sequelize), + }, { transaction }); + } + + if (!(await queryInterface.tableExists('weavejs_video'))) { + await queryInterface.createTable('weavejs_video', { + roomId: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + ...mediaColumns(Sequelize, 'videoId'), + ...timestampColumns(Sequelize), + }, { transaction }); + } + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down (queryInterface) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + if (await queryInterface.tableExists('weavejs_thread_answer')) { + await queryInterface.dropTable('weavejs_thread_answer', { transaction }); + } + if (await queryInterface.tableExists('weavejs_thread')) { + await queryInterface.dropTable('weavejs_thread', { transaction }); + } + if (await queryInterface.tableExists('weavejs_connection')) { + await queryInterface.dropTable('weavejs_connection', { transaction }); + } + if (await queryInterface.tableExists('weavejs_image')) { + await queryInterface.dropTable('weavejs_image', { transaction }); + } + if (await queryInterface.tableExists('weavejs_video')) { + await queryInterface.dropTable('weavejs_video', { transaction }); + } + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/code/package.json b/code/package.json index 54df8fbd..3161f64c 100644 --- a/code/package.json +++ b/code/package.json @@ -41,7 +41,7 @@ "publish:snapshot": "nx run-many -t publish:snapshot -- --verbose --tag next --registry $NPM_PUBLISHING_REGISTRY --unsafe-perm", "start": "node --experimental-specifier-resolution=node --env-file=.env --loader ts-node/esm dist/server.js", "storage:cleanup": "tsx --env-file=./src/actions/storage-cleanup/.env ./src/actions/storage-cleanup/action.ts", - "verify": "npm ci && npm run lint && npm run build", + "verify": "npm ci --force && npm run lint && npm run build", "version:development": "export BUMP_DEVELOP_VERSION=$(npm version $(npm version minor)-SNAPSHOT) && lerna version $BUMP_DEVELOP_VERSION --no-git-tag-version --no-push --force-publish --exact --yes && npm version $(npm version minor)-SNAPSHOT", "version:release": "npm ci && lerna version --no-git-tag-version --no-push --force-publish --exact --yes $RELEASE_VERSION && npm version $RELEASE_VERSION" }, diff --git a/code/vite.config.js b/code/vite.config.js index ed5e198e..3f5ee6f5 100644 --- a/code/vite.config.js +++ b/code/vite.config.js @@ -28,7 +28,7 @@ export default defineConfig(({ mode }) => { WEAVEJS_REPO_PATH, "renderer-konva-react-reconciler/src", ), - "@inditextech/weave-sdk/server": path.resolve(WEAVEJS_REPO_PATH, "sdk/src/"), + "@inditextech/weave-sdk/server": path.resolve(WEAVEJS_REPO_PATH, "sdk/src/index.node.ts"), "@inditextech/weave-sdk": path.resolve(WEAVEJS_REPO_PATH, "sdk/src"), "@inditextech/weave-store-azure-web-pubsub/client": path.resolve( WEAVEJS_REPO_PATH,