Skip to content

Commit 79cf8c9

Browse files
authored
Merge pull request #902 from ELEVATE-Project/staging
release: v3.5.0 – staging → master
2 parents bc7f2be + 8e1b61f commit 79cf8c9

23 files changed

Lines changed: 3275 additions & 170 deletions

AGENTS.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# AGENTS.md
2+
3+
## Purpose
4+
5+
This repository contains the ELEVATE User Service (`com.shikshalokam.mentoring.userservice`), a Node.js/Express API for user, organization, role/permission, invite, and tenant workflows.
6+
7+
## Repository Layout
8+
9+
- `src/` - Main service codebase (this is where almost all work happens).
10+
- `src/app.js` - Service entrypoint.
11+
- `src/routes/index.js` - Dynamic API router and global error handling.
12+
- `src/controllers/v1/` - Versioned controller endpoints.
13+
- `src/services/` - Business logic layer.
14+
- `src/database/` - Sequelize models, queries, migrations, seeders.
15+
- `src/middlewares/` - Express middlewares: `authenticator.js` (JWT/auth), `pagination.js`, `validator.js`. All routes pass through these; touch with care.
16+
- `src/validators/v1/` - Request validation schemas (one per controller). Every new endpoint should have a matching validator here.
17+
- `src/generics/` - Shared infrastructure utilities: `utils.js` (large general helpers), `materializedViews.js`, `kafka-communication.js`, `redis-communication.js`, `RollbackStack.js`. Check here before writing new utility code.
18+
- `src/dtos/` - Data transfer objects for user, org, tenant, and events. Update when adding or changing fields in API responses.
19+
- `src/constants/` - App-wide constants: `common.js`, `blacklistConfig.js`, endpoint definitions. Don't define new constants inline in services/controllers.
20+
- `src/locales/` - i18n string files (`en.json`, `hi.json`). All user-facing messages must be added here, not hardcoded.
21+
- `src/configs/` - Kafka, Redis, cache, cloud-storage, queue worker setup.
22+
- `src/scripts/` - Operational scripts and data migration utilities.
23+
- `src/health-checks/` - Health check endpoints and config.
24+
- `dev-ops/` - Dependency docker compose and reporting helpers.
25+
- `README.md` - Full setup docs and dependency installation notes.
26+
27+
## Stack and Runtime
28+
29+
- Node.js 20 (recommended in `README.md`)
30+
- Express 4
31+
- PostgreSQL (with Citus in some deployments)
32+
- Sequelize + `sequelize-cli`
33+
- Redis
34+
- Kafka (`kafkajs`)
35+
- BullMQ worker (invites and bulk-user workflows)
36+
37+
## Critical Context
38+
39+
- Run service commands from `src/`, not repo root.
40+
- Repo root has a minimal `package.json` used for tooling; the real app package is `src/package.json`.
41+
- App startup validates env vars via `src/envVariables.js`; missing required vars will stop boot.
42+
- Route format is dynamic:
43+
- `${APPLICATION_BASE_URL}/:version/:controller/:method`
44+
- `${APPLICATION_BASE_URL}/:version/:controller/:file/:method`
45+
46+
## Local Setup (Fast Path)
47+
48+
1. `cd src`
49+
2. `cp .env.sample .env`
50+
3. Fill required env values (see `src/envVariables.js` and `src/.env.sample`)
51+
4. `npm install`
52+
5. `npm run db:migrate`
53+
6. `npm run db:seed:all` (optional but common for local)
54+
7. `npm start`
55+
56+
Default local app URL is typically `http://localhost:3001` (or `APPLICATION_PORT` from `.env`).
57+
58+
## Core Commands
59+
60+
From `src/`:
61+
62+
- `npm start` - run in development with nodemon
63+
- `npm run prod` - production mode
64+
- `npm run qa` / `npm run stage` - environment-specific starts
65+
- `npm run db:init` - create DB + migrate
66+
- `npm run db:migrate` - run migrations
67+
- `npm run db:seed:all` - run all seeders
68+
> **Note:** Tests (unit and integration) are currently broken and should not be run. Omit any test steps until further notice.
69+
70+
## Important Env Groups
71+
72+
See full list in `src/envVariables.js`; key groups:
73+
74+
- App/Auth: `APPLICATION_*`, `ACCESS_TOKEN_*`, `REFRESH_TOKEN_*`, `API_DOC_URL`
75+
- Database: `DEV_DATABASE_URL`, `TEST_DATABASE_URL`, `DATABASE_URL`, `DB_POOL_*`
76+
- Kafka: `KAFKA_URL`, `KAFKA_GROUP_ID`, event topic toggles and topic names
77+
- Redis/Cache: `REDIS_HOST`, `INTERNAL_CACHE_EXP_TIME`
78+
- Storage: `CLOUD_STORAGE_PROVIDER`, `CLOUD_STORAGE_*`, `PUBLIC_ASSET_BUCKETNAME`
79+
- Integrations: `MENTORING_SERVICE_URL`, `ENTITY_MANAGEMENT_SERVICE_BASE_URL`, scheduler and notification vars
80+
81+
## Health Endpoints
82+
83+
- `GET /health`
84+
- `GET /healthCheckStatus`
85+
86+
Health config lives in `src/health-checks/health.config.js` and currently checks kafka, redis, postgres, plus dependent services.
87+
88+
## Operational Scripts
89+
90+
From `src/`:
91+
92+
- `npm run migrate:tenant-org-data` - tenant/org data move script
93+
- `npm run check:user-in-account-search -- --auth-token=<token> ...` - paginated account search checker
94+
- `node scripts/insertDefaultOrg.js` - bootstrap default org
95+
- `node scripts/encryptDecryptEmails.js encrypt|decrypt`
96+
97+
More script notes: `src/scripts/readme.md`.
98+
99+
## Code Style and Tooling
100+
101+
- ESLint rules: `src/.eslintrc.json` (tabs, single quotes, no semicolons)
102+
- Prettier: `.prettierrc.json` at repo root
103+
- Husky pre-commit runs `lint-staged` from `src/`
104+
105+
## Module Aliases
106+
107+
Defined in `src/package.json` under `_moduleAliases`. Always use these instead of relative paths:
108+
109+
| Alias | Resolves to |
110+
| ---------------- | -------------------- |
111+
| `@root` | `src/` |
112+
| `@configs` | `src/configs/` |
113+
| `@constants` | `src/constants/` |
114+
| `@controllers` | `src/controllers/` |
115+
| `@database` | `src/database/` |
116+
| `@generics` | `src/generics/` |
117+
| `@health-checks` | `src/health-checks/` |
118+
| `@middlewares` | `src/middlewares/` |
119+
| `@routes` | `src/routes/` |
120+
| `@services` | `src/services/` |
121+
| `@validators` | `src/validators/` |
122+
| `@utils` | `src/utils/` |
123+
| `@helpers` | `src/helpers/` |
124+
| `@scripts` | `src/scripts/` |
125+
| `@dtos` | `src/dtos/` |
126+
| `@public` | `src/public/` |
127+
128+
## Implementation Guardrails
129+
130+
- Keep controller-service-query layering intact.
131+
- Preserve response shape used by router/error middleware (`statusCode`, `responseCode`, `message`, `result`, `meta`).
132+
- For DB schema changes, add Sequelize migrations in `src/database/migrations/`.
133+
- Prefer updating existing query/service modules instead of embedding raw SQL in controllers.
134+
- If changing env requirements, update both `src/envVariables.js` and `src/.env.sample`.
135+
- All user-facing strings must be added to `src/locales/en.json` (and `hi.json` if translatable). Never hardcode message strings in services or controllers.
136+
137+
## Pull Request Instructions
138+
139+
1. Keep PRs focused on one logical change (avoid mixing refactor + feature + migration unless required).
140+
2. Rebase/sync with latest target branch before opening PR.
141+
3. Run lint validation from `src/`:
142+
- `npx eslint .` (if lint-sensitive files changed)
143+
> **Note:** Unit and integration tests are currently broken — skip test steps.
144+
4. Include migration/rollback notes in PR description when touching `src/database/migrations/`.
145+
5. If env/config changes are introduced, update:
146+
- `src/.env.sample`
147+
- `src/envVariables.js`
148+
- relevant README/notes
149+
6. PR description should include:
150+
- What changed
151+
- Why it changed
152+
- Risk/impact
153+
- Test evidence (commands + summary, if applicable — tests currently broken)
154+
- API contract changes (if any)
155+
156+
## Commit Message Format
157+
158+
Use Conventional Commit style:
159+
160+
- `<type>(<scope>): <subject>`
161+
162+
Example:
163+
164+
- `refactor(organization): optimize feature access logic for role mappings`
165+
166+
Allowed `type` values:
167+
168+
- `feat`, `fix`, `refactor`, `perf`, `test`, `docs`, `chore`, `build`, `ci`
169+
170+
Scope guidance:
171+
172+
- Use module/domain names such as `organization`, `user`, `tenant`, `roles`, `scripts`, `migrations`, `health-checks`, `configs`.
173+
174+
Subject guidance:
175+
176+
- Use imperative mood and keep it concise.
177+
- Do not end subject with a period.
178+
179+
## Useful References
180+
181+
- Main setup and infra guidance: `README.md`
182+
- Sequelize path mapping: `src/.sequelizerc`
183+
- Citus distribution SQL helper: `src/distributionColumns.sql`
184+
- Health check guide: `src/health-checks/README.md`

src/constants/common.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,20 @@ module.exports = {
116116
SIGNEDUP_STATUS: 'SIGNEDUP',
117117
SEQUELIZE_UNIQUE_CONSTRAINT_ERROR: 'SequelizeUniqueConstraintError',
118118
SEQUELIZE_UNIQUE_CONSTRAINT_ERROR_CODE: 'ER_DUP_ENTRY',
119+
DEFAULT_TENANT_CONFIGURATION: {
120+
allowed_auth_mode: process.env.DEFAULT_ALLOWED_AUTH_MODES.split(','),
121+
auto_register: process.env.DEFAULT_AUTO_REGISTER === 'true',
122+
},
123+
AUTH_MODES: {
124+
OTP: 'otp',
125+
PASSWORD: 'password',
126+
},
127+
OTP_PURPOSES: {
128+
SIGNUP: 'signup',
129+
LOGIN: 'login',
130+
},
131+
EMAIL: 'email',
132+
PHONE: 'phone',
133+
USER_NAME: 'username',
134+
USER: 'User',
119135
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict'
2+
3+
const common = require('@constants/common')
4+
5+
module.exports = {
6+
up: async (queryInterface, Sequelize) => {
7+
await queryInterface.addColumn('tenants', 'configuration', {
8+
type: Sequelize.JSONB,
9+
allowNull: false,
10+
defaultValue: common.DEFAULT_TENANT_CONFIGURATION,
11+
})
12+
13+
await queryInterface.sequelize.query(
14+
`UPDATE tenants
15+
SET configuration = :configuration
16+
WHERE configuration IS NULL`,
17+
{
18+
replacements: {
19+
configuration: JSON.stringify(common.DEFAULT_TENANT_CONFIGURATION),
20+
},
21+
}
22+
)
23+
},
24+
25+
down: async (queryInterface) => {
26+
await queryInterface.removeColumn('tenants', 'configuration')
27+
},
28+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict'
2+
3+
module.exports = {
4+
up: async (queryInterface, Sequelize) => {
5+
await queryInterface.sequelize.query('DROP MATERIALIZED VIEW IF EXISTS m_users;')
6+
7+
await queryInterface.changeColumn('users', 'name', {
8+
type: Sequelize.STRING,
9+
allowNull: true,
10+
})
11+
12+
await queryInterface.changeColumn('users', 'password', {
13+
type: Sequelize.STRING,
14+
allowNull: true,
15+
})
16+
17+
await queryInterface.changeColumn('users_credentials', 'password', {
18+
type: Sequelize.STRING,
19+
allowNull: true,
20+
})
21+
},
22+
23+
down: async (queryInterface, Sequelize) => {
24+
await queryInterface.sequelize.query('DROP MATERIALIZED VIEW IF EXISTS m_users;')
25+
26+
await queryInterface.sequelize.query(`UPDATE users SET name = '' WHERE name IS NULL`)
27+
await queryInterface.sequelize.query(`UPDATE users SET password = '' WHERE password IS NULL`)
28+
await queryInterface.sequelize.query(`UPDATE users_credentials SET password = '' WHERE password IS NULL`)
29+
await queryInterface.changeColumn('users', 'name', {
30+
type: Sequelize.STRING,
31+
allowNull: false,
32+
})
33+
34+
await queryInterface.changeColumn('users', 'password', {
35+
type: Sequelize.STRING,
36+
allowNull: false,
37+
})
38+
39+
await queryInterface.changeColumn('users_credentials', 'password', {
40+
type: Sequelize.STRING,
41+
allowNull: false,
42+
})
43+
},
44+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
'use strict'
2+
3+
module.exports = {
4+
async up(queryInterface, Sequelize) {
5+
const transaction = await queryInterface.sequelize.transaction()
6+
try {
7+
const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE
8+
9+
const defaultOrgsPerTenant = await queryInterface.sequelize.query(
10+
'SELECT id, code, tenant_code FROM organizations WHERE code = :defaultOrgCode',
11+
{
12+
replacements: { defaultOrgCode },
13+
type: queryInterface.sequelize.QueryTypes.SELECT,
14+
transaction,
15+
}
16+
)
17+
18+
if (!defaultOrgsPerTenant.length) {
19+
console.warn('No default organizations found across tenants. Skipping migration.')
20+
await transaction.commit()
21+
return
22+
}
23+
24+
const now = new Date()
25+
const entityTypesToInsert = defaultOrgsPerTenant.map((org) => ({
26+
value: 'name',
27+
label: 'Name',
28+
status: 'ACTIVE',
29+
created_by: null,
30+
updated_by: null,
31+
allow_filtering: false,
32+
data_type: 'STRING',
33+
organization_id: org.id,
34+
organization_code: org.code,
35+
tenant_code: org.tenant_code,
36+
parent_id: null,
37+
has_entities: false,
38+
allow_custom_entities: false,
39+
model_names: ['User'],
40+
created_at: now,
41+
updated_at: now,
42+
}))
43+
44+
await queryInterface.bulkInsert('entity_types', entityTypesToInsert, { transaction })
45+
46+
await transaction.commit()
47+
} catch (error) {
48+
await transaction.rollback()
49+
console.error('Migration up failed:', error)
50+
throw error
51+
}
52+
},
53+
54+
async down(queryInterface, Sequelize) {
55+
const transaction = await queryInterface.sequelize.transaction()
56+
try {
57+
await queryInterface.bulkDelete(
58+
'entity_types',
59+
{
60+
value: 'name',
61+
},
62+
{ transaction }
63+
)
64+
await transaction.commit()
65+
} catch (error) {
66+
await transaction.rollback()
67+
console.error('Migration down failed:', error)
68+
throw error
69+
}
70+
},
71+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict'
2+
3+
const common = require('@constants/common')
4+
5+
module.exports = {
6+
up: async (queryInterface, Sequelize) => {
7+
await queryInterface.changeColumn('tenants', 'configuration', {
8+
type: Sequelize.JSONB,
9+
allowNull: false,
10+
defaultValue: null,
11+
})
12+
13+
await queryInterface.sequelize.query(
14+
`UPDATE tenants
15+
SET configuration = jsonb_set(
16+
jsonb_set(configuration, '{auto_register}', 'false'::jsonb),
17+
'{allowed_auth_mode}', '["password"]'::jsonb
18+
)`
19+
)
20+
},
21+
22+
down: async (queryInterface, Sequelize) => {
23+
await queryInterface.changeColumn('tenants', 'configuration', {
24+
type: Sequelize.JSONB,
25+
allowNull: false,
26+
defaultValue: common.DEFAULT_TENANT_CONFIGURATION,
27+
})
28+
29+
await queryInterface.sequelize.query(
30+
`UPDATE tenants
31+
SET configuration = jsonb_set(
32+
jsonb_set(configuration, '{auto_register}', 'true'::jsonb),
33+
'{allowed_auth_mode}', '["otp","password"]'::jsonb)`
34+
)
35+
},
36+
}

0 commit comments

Comments
 (0)